This commit is contained in:
parent
5703fded24
commit
12cd774816
@ -52,6 +52,12 @@ type userCreateRequest struct {
|
||||
Role string `json:"role"`
|
||||
}
|
||||
|
||||
type userEditRequest struct {
|
||||
userCreateRequest
|
||||
GroupIDs *[]int `json:"group_ids"`
|
||||
MembershipVersion int `json:"membership_version"`
|
||||
}
|
||||
|
||||
func CreateUser(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
targetRole := access.RoleUser
|
||||
var reqData userCreateRequest
|
||||
@ -283,7 +289,7 @@ func Info(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
|
||||
func EditUser(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
var reqData userCreateRequest
|
||||
var reqData userEditRequest
|
||||
if err := decodeUserRequest(w, req, &reqData); err != nil {
|
||||
response.NewErrorResponse(response.ParamsError, "参数错误", "").FPrint(w)
|
||||
return
|
||||
@ -321,6 +327,15 @@ func EditUser(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
targetRole := user.EffectiveRole()
|
||||
writeGuard := accountWriteGuard(ctx, user)
|
||||
if reqData.GroupIDs != nil {
|
||||
if !requireAccountGroupManagement(ctx, w) {
|
||||
return
|
||||
}
|
||||
if reqData.MembershipVersion < 1 || reqData.MembershipVersion != user.SecurityVersion {
|
||||
writeAccountGroupError(w, accountgroup.ErrConflict)
|
||||
return
|
||||
}
|
||||
}
|
||||
if !requireAccountScope(ctx, w, user.ID) {
|
||||
return
|
||||
}
|
||||
@ -394,7 +409,7 @@ func EditUser(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
now := time.Now().UTC()
|
||||
user.PasswordChangedAt = &now
|
||||
}
|
||||
if reqData.Password != "" || user.Disabled != originalDisabled || user.EffectiveRole() != originalRole {
|
||||
if reqData.Password != "" || user.Disabled != originalDisabled || user.EffectiveRole() != originalRole || reqData.GroupIDs != nil {
|
||||
user.SessionVersion = originalSessionVersion + 1
|
||||
user.SecurityVersion = originalSecurityVersion + 1
|
||||
}
|
||||
@ -430,7 +445,7 @@ func EditUser(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
if reqData.Password != "" {
|
||||
updateColumns = append(updateColumns, "password", "must_change_password", "password_changed_at")
|
||||
}
|
||||
if reqData.Password != "" || user.Disabled != originalDisabled || user.EffectiveRole() != originalRole {
|
||||
if reqData.Password != "" || user.Disabled != originalDisabled || user.EffectiveRole() != originalRole || reqData.GroupIDs != nil {
|
||||
updateColumns = append(updateColumns, "session_version", "security_version")
|
||||
}
|
||||
update := tx.ID(user.ID).Where("session_version = ? AND security_version = ?", originalSessionVersion, originalSecurityVersion).Cols(updateColumns...)
|
||||
@ -453,6 +468,18 @@ func EditUser(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
return
|
||||
}
|
||||
}
|
||||
// 角色和分组在同一事务中校验与更新,任何失败均不能留下部分修改。
|
||||
if reqData.GroupIDs != nil {
|
||||
ids, err := accountgroup.ValidateMembership(tx, user.EffectiveRole(), *reqData.GroupIDs)
|
||||
if err == nil {
|
||||
err = accountgroup.ReplaceMembership(tx, user.ID, ids)
|
||||
}
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
writeAccountGroupError(w, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if user.EffectiveRole() != access.RoleAdmin {
|
||||
count, err := tx.Where("user_id = ?", user.ID).Count(&models.AccountGroupMember{})
|
||||
if err != nil {
|
||||
|
||||
@ -12,7 +12,13 @@ export function useAccountGroups(globalStatus, onMembershipSaved = () => {}) {
|
||||
const membershipVisible = ref(false)
|
||||
const membershipUser = ref(null)
|
||||
const membershipSelection = ref([])
|
||||
const membershipDraft = ref(false)
|
||||
let applyMembershipDraft = null
|
||||
const membershipLimit = computed(() => membershipUser.value?.Role === 'admin' ? 100 : 1)
|
||||
const membershipSingleSelection = computed({
|
||||
get: () => membershipSelection.value.length === 1 ? membershipSelection.value[0] : null,
|
||||
set: value => { membershipSelection.value = [value] }
|
||||
})
|
||||
let generation = 0
|
||||
let alive = true
|
||||
onBeforeUnmount(() => { alive = false; generation++ })
|
||||
@ -107,16 +113,27 @@ export function useAccountGroups(globalStatus, onMembershipSaved = () => {}) {
|
||||
}
|
||||
}
|
||||
|
||||
const editMembership = user => {
|
||||
const editMembership = (user, options = {}) => {
|
||||
if (!canManageGroups.value || groupSaving.value) return
|
||||
membershipUser.value = user
|
||||
membershipSelection.value = [...(user.GroupIDs || [0])]
|
||||
membershipUser.value = { ...user, Role: options.role || user.Role }
|
||||
membershipSelection.value = [...(options.groupIDs || user.GroupIDs || [0])]
|
||||
applyMembershipDraft = options.onApply || null
|
||||
membershipDraft.value = Boolean(applyMembershipDraft)
|
||||
membershipVisible.value = true
|
||||
}
|
||||
|
||||
const saveMembership = async () => {
|
||||
if (!canManageGroups.value || groupSaving.value || !membershipUser.value) return
|
||||
if (membershipSelection.value.length === 0) membershipSelection.value = [0]
|
||||
if (membershipSelection.value.length > membershipLimit.value) {
|
||||
ElMessage.error('普通用户只能选择一个分组,请重新选择')
|
||||
return
|
||||
}
|
||||
if (applyMembershipDraft) {
|
||||
applyMembershipDraft([...membershipSelection.value])
|
||||
membershipVisible.value = false
|
||||
return
|
||||
}
|
||||
groupSaving.value = true
|
||||
try {
|
||||
const response = await requestWithRecentAuthentication(() => http.post('/api/v1/user/groups/membership', {
|
||||
@ -145,6 +162,7 @@ export function useAccountGroups(globalStatus, onMembershipSaved = () => {}) {
|
||||
return {
|
||||
accountGroups, groupsLoading, groupsError, canManageGroups, groupSaving,
|
||||
membershipVisible, membershipUser, membershipSelection, membershipLimit,
|
||||
membershipDraft, membershipSingleSelection,
|
||||
refreshGroups, loadGroup, toggleGroup, saveGroup, editMembership, saveMembership
|
||||
}
|
||||
}
|
||||
|
||||
@ -214,8 +214,49 @@
|
||||
outline-offset: 4px;
|
||||
}
|
||||
|
||||
.membership-select {
|
||||
.membership-options {
|
||||
width: 100%;
|
||||
max-height: 260px;
|
||||
overflow-y: auto;
|
||||
margin: 0;
|
||||
padding: 8px 12px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid var(--pm-border-color);
|
||||
border-radius: var(--pm-radius-sm);
|
||||
background: var(--pm-bg-secondary);
|
||||
}
|
||||
|
||||
.membership-options-title {
|
||||
padding: 0 6px;
|
||||
color: var(--pm-text-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.membership-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 40px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.membership-option:hover {
|
||||
background: var(--el-fill-color);
|
||||
}
|
||||
|
||||
.membership-option input {
|
||||
flex: 0 0 auto;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
accent-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.membership-option:focus-within {
|
||||
outline: 2px solid var(--el-color-primary);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.membership-summary {
|
||||
|
||||
@ -57,20 +57,31 @@ export function usePage() {
|
||||
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) return []
|
||||
return groups.accountGroups.value.filter(group =>
|
||||
groups.canManageGroups.value
|
||||
? (user.GroupIDs || [0]).includes(group.id)
|
||||
? editingGroupIDs.value.includes(group.id)
|
||||
: group.rows.some(member => member.ID === user.ID)
|
||||
)
|
||||
})
|
||||
|
||||
const openMembershipDialog = () => {
|
||||
if (submitting.value || !selectedUser.value) return
|
||||
groups.editMembership(selectedUser.value)
|
||||
groups.editMembership(selectedUser.value, {
|
||||
role: editUserInfo.role,
|
||||
groupIDs: editingGroupIDs.value,
|
||||
onApply: ids => { editingGroupIDs.value = ids }
|
||||
})
|
||||
}
|
||||
|
||||
const recoveryCodeDialogVisible = ref(false)
|
||||
@ -122,6 +133,8 @@ export function usePage() {
|
||||
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
|
||||
@ -193,6 +206,13 @@ export function usePage() {
|
||||
}
|
||||
if (creating || roleChanged.value) newData.role = editUserInfo.role
|
||||
if (creating || editUserInfo.password !== '') newData.password = editUserInfo.password
|
||||
if (!creating && groups.canManageGroups.value && (membershipChanged.value || roleChanged.value)) {
|
||||
if (editUserInfo.role !== 'admin' && editingGroupIDs.value.length !== 1) {
|
||||
throw new Error('普通用户只能属于一个分组,请在分组管理中选择一个分组')
|
||||
}
|
||||
newData.group_ids = [...editingGroupIDs.value]
|
||||
newData.membership_version = editingMembershipVersion.value
|
||||
}
|
||||
const res = await requestWithRecentAuthentication(() => creating
|
||||
? http.post('/api/v1/user/create', newData)
|
||||
: http.post('/api/v1/user/edit', newData))
|
||||
|
||||
@ -123,16 +123,44 @@
|
||||
<p>账号:{{ membershipUser?.Account }}</p>
|
||||
<el-form label-position="top">
|
||||
<el-form-item label="所属分组">
|
||||
<el-select v-model="membershipSelection" multiple :multiple-limit="membershipLimit" :disabled="groupSaving" placeholder="未选择时归入默认分组" class="membership-select">
|
||||
<el-option v-for="group in accountGroups" :key="group.id" :label="group.name" :value="group.id" />
|
||||
</el-select>
|
||||
<fieldset class="membership-options" :disabled="groupSaving">
|
||||
<legend class="membership-options-title">
|
||||
{{ membershipLimit > 1 ? '管理员可勾选多个分组' : '请选择一个分组' }}
|
||||
</legend>
|
||||
<label
|
||||
v-for="group in accountGroups"
|
||||
:key="group.id"
|
||||
class="membership-option"
|
||||
>
|
||||
<input
|
||||
v-if="membershipLimit > 1"
|
||||
v-model="membershipSelection"
|
||||
type="checkbox"
|
||||
:value="group.id"
|
||||
:disabled="membershipSelection.length >= membershipLimit && !membershipSelection.includes(group.id)"
|
||||
/>
|
||||
<input
|
||||
v-else
|
||||
v-model="membershipSingleSelection"
|
||||
type="radio"
|
||||
name="account-membership"
|
||||
:value="group.id"
|
||||
/>
|
||||
<span>{{ group.name }}</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<p class="settings-desc">管理员可以多选;普通用户只能选择一个分组。</p>
|
||||
<p v-if="membershipDraft" class="settings-desc">
|
||||
此处仅应用到编辑草稿,点击编辑用户的“确认”后,权限级别和分组才会一起保存。
|
||||
</p>
|
||||
<el-alert title="修改归属会使该账号的旧登录会话和通知发信凭据失效,需要重新登录并配置通知凭据。" type="warning" :closable="false" />
|
||||
<template #footer>
|
||||
<el-button :disabled="groupSaving" @click="membershipVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="groupSaving" @click="saveMembership">保存分组</el-button>
|
||||
<el-button type="primary" :loading="groupSaving" @click="saveMembership">
|
||||
{{ membershipDraft ? '应用到编辑' : '保存分组' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
@ -213,7 +241,7 @@
|
||||
</el-button>
|
||||
</div>
|
||||
<p v-if="canManageGroups" class="membership-hint">
|
||||
分组单独保存,不会提交本窗口其他修改;选择数量按账号已保存的权限级别校验。
|
||||
分组选择跟随当前权限级别,点击“确认”后与其他修改一起保存;取消则不保存。
|
||||
</p>
|
||||
<p v-else class="membership-hint">
|
||||
仅显示你有权查看的分组,成员归属由超级管理员维护。
|
||||
@ -312,6 +340,8 @@ const {
|
||||
membershipUser,
|
||||
membershipSelection,
|
||||
membershipLimit,
|
||||
membershipDraft,
|
||||
membershipSingleSelection,
|
||||
loadGroup,
|
||||
toggleGroup,
|
||||
saveGroup,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user