补齐新增账号配置入口及分组事务保存
Some checks are pending
Security Checks / security (push) Waiting to run

This commit is contained in:
smail 2026-09-26 12:05:47 +08:00
parent caf20a1186
commit 6761ab2091
6 changed files with 137 additions and 23 deletions

View File

@ -44,6 +44,7 @@ func decodeUserRequest(w http.ResponseWriter, req *http.Request, dst any) error
}
type userCreateRequest struct {
GroupIDs *[]int `json:"group_ids"`
Id int `json:"id"`
Account string `json:"account"`
Username string `json:"username"`
@ -93,6 +94,15 @@ func CreateUser(ctx *context.Context, w http.ResponseWriter, req *http.Request)
}
user := models.User{Name: username, Account: account, Disabled: reqData.Disabled, SessionVersion: 1, SecurityVersion: 1}
user.ApplyRole(targetRole)
groupIDs := []int{0}
if reqData.GroupIDs != nil {
groupIDs = *reqData.GroupIDs
}
if len(groupIDs) > 0 && (len(groupIDs) != 1 || groupIDs[0] != 0) {
if !requireAccountGroupManagement(ctx, w) {
return
}
}
if err := auth.SetPassword(&user, reqData.Password); err != nil {
if writePasswordBusy(w, err) {
return
@ -105,12 +115,46 @@ func CreateUser(ctx *context.Context, w http.ResponseWriter, req *http.Request)
response.NewErrorResponse(response.ServerError, "密码设置失败", "").FPrint(w)
return
}
if _, err := db.Instance.Insert(&user); err != nil {
if db.Instance == nil {
writeAccountGroupError(w, accountgroup.ErrDenied)
return
}
tx := db.Instance.NewSession()
defer tx.Close()
if err := tx.Begin(); err != nil {
writeAccountGroupError(w, err)
return
}
defer tx.Rollback()
actor := models.User{ID: ctx.UserID, SessionVersion: ctx.SessionVersion, SecurityVersion: ctx.SecurityVersion}
actor.ApplyRole(ctx.EffectiveRole())
if err := accountgroup.CheckWrite(tx, accountWriteGuard(ctx, actor)); err != nil {
writeAccountGroupError(w, err)
return
}
groupIDs, err = accountgroup.ValidateMembership(tx, user.EffectiveRole(), groupIDs)
if err != nil {
writeAccountGroupError(w, err)
return
}
if _, err := tx.Insert(&user); err != nil {
log.WithContext(ctx).WithError(err).Error("create user failed")
response.NewErrorResponse(response.ServerError, "创建用户失败", "").FPrint(w)
return
}
response.NewSuccessResponse(user).FPrint(w)
if err := accountgroup.ReplaceMembership(tx, user.ID, groupIDs); err != nil {
writeAccountGroupError(w, err)
return
}
if err := tx.Commit(); err != nil {
writeAccountGroupError(w, err)
return
}
response.NewSuccessResponse(struct {
models.User
GroupIDs []int
MembershipVersion int
}{user, groupIDs, user.SecurityVersion}).FPrint(w)
}
type userListRequest struct {

View File

@ -4,7 +4,7 @@ import { http } from '@/utils/axios'
import { useGlobalStatusStore } from '@/stores/useGlobalStatusStore'
import { isRecentAuthenticationCancelled, requestWithRecentAuthentication } from '@/utils/recentAuthentication'
export function usePage() {
export function usePage(props = {}) {
const globalStatus = useGlobalStatusStore()
const manager = computed(() => globalStatus.hasCapability('notification.manage'))
const loading = ref(false)
@ -14,7 +14,7 @@ export function usePage() {
const accounts = ref([])
const page = ref(1)
const total = ref(0)
const selectedID = ref(Number(globalStatus.userInfos?.id || 0))
const selectedID = ref(Number(props.userId || globalStatus.userInfos?.id || 0))
const form = reactive({ enabled: false, minute_limit: 10, day_limit: 100 })
const secret = ref('')
const secretAccount = ref('')
@ -70,7 +70,7 @@ export function usePage() {
}
}
const loadAccounts = async () => {
if (!manager.value) return
if (!manager.value || props.userId) return
const sequence = ++accountsSequence
try {
const data = dataOf(await http.get('/api/v1/system/notification/accounts', { params: { page: page.value } }))

View File

@ -16,7 +16,7 @@
:closable="false"
/>
<div v-if="manager" class="notification-panel">
<div v-if="manager && !userId" class="notification-panel">
<h3>账号授权管理</h3>
<el-table :data="accounts" stripe>
<el-table-column prop="account" label="账号" min-width="110" />
@ -111,11 +111,13 @@
<script setup>
import { usePage } from './SystemNotification.js'
const props = defineProps({ userId: { type: Number, default: 0 } })
const {
manager, loading, submitting, error, status, accounts, page, total, form,
secret, secretAccount, secretVisible, seconds, canManage, stateText, dirty,
loadAccounts, selectAccount, refresh, save, rotate, clearSecret, copySecret, beforeLeave
} = usePage()
} = usePage(props)
defineExpose({ beforeLeave })
</script>

View File

@ -305,3 +305,23 @@
flex: 0 0 auto;
margin: 0;
}
.account-form {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
column-gap: 18px;
}
.account-form-wide {
grid-column: 1 / -1;
}
.account-form > :deep(.el-form-item) {
min-width: 0;
}
@media (max-width: 560px) {
.account-form {
grid-template-columns: minmax(0, 1fr);
}
}

View File

@ -67,7 +67,7 @@ export function usePage() {
const selectedMembershipGroups = computed(() => {
const user = selectedUser.value
if (!user) return []
if (!user && editModel.value !== 'create') return []
return groups.accountGroups.value.filter(group =>
groups.canManageGroups.value
? editingGroupIDs.value.includes(group.id)
@ -76,8 +76,8 @@ export function usePage() {
})
const openMembershipDialog = () => {
if (submitting.value || !selectedUser.value) return
groups.editMembership(selectedUser.value, {
if (submitting.value || (!selectedUser.value && editModel.value !== 'create')) return
groups.editMembership(selectedUser.value || { Role: editUserInfo.role }, {
role: editUserInfo.role,
groupIDs: editingGroupIDs.value,
onApply: ids => { editingGroupIDs.value = ids }
@ -88,6 +88,13 @@ export function usePage() {
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)
@ -148,6 +155,8 @@ export function usePage() {
const createUser = function () {
if (!canCreateUser.value) return
selectedUser.value = null
editingGroupIDs.value = [0]
editingMembershipVersion.value = 0
editUserInfo.account = ""
editUserInfo.name = ""
editUserInfo.disabled = false
@ -206,12 +215,12 @@ 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 (groups.canManageGroups.value && (creating || 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
if (!creating) newData.membership_version = editingMembershipVersion.value
}
const res = await requestWithRecentAuthentication(() => creating
? http.post('/api/v1/user/create', newData)
@ -221,8 +230,12 @@ export function usePage() {
}
ElNotification({ title: lang.succ, message: '用户信息已保存', type: 'success' })
editUserInfo.password = ''
reflushList()
userInfoDialog.value = false
if (creating) {
handleEdit(0, res.data)
} else {
userInfoDialog.value = false
}
await reflushList()
} catch (error) {
if (!isRecentAuthenticationCancelled(error)) {
ElNotification({
@ -262,6 +275,10 @@ export function usePage() {
recoveryCodeWebManaged,
recoveryCodeDialogVisible,
externalEmailDialogVisible,
notificationDialogVisible,
notificationPanel,
canManageNotification,
closeNotificationDialog,
securityDialogOpening,
openRecoveryCodeDialog,
openExternalEmailDialog,

View File

@ -162,14 +162,14 @@
<el-dialog
v-model="userInfoDialog"
:title="title"
width="450px"
width="min(640px, 94vw)"
class="premium-dialog"
:close-on-click-modal="!submitting"
:close-on-press-escape="!submitting"
:show-close="!submitting"
>
<div class="dialog-content">
<el-form label-position="top">
<el-form label-position="top" class="account-form">
<el-form-item :label="lang.account">
<el-input :disabled="editModel === 'edit' || submitting" v-model="editUserInfo.account" />
</el-form-item>
@ -211,7 +211,7 @@
</span>
</el-form-item>
<el-form-item v-if="editModel === 'edit'" label="所属分组">
<el-form-item label="所属分组" class="account-form-wide">
<div class="membership-summary">
<div class="membership-tags">
<el-tag
@ -237,21 +237,24 @@
</div>
</el-form-item>
<div v-if="editModel === 'edit'" class="security-edit-grid">
<div class="security-edit-grid account-form-wide">
<el-tag v-if="editModel === 'create'" class="account-form-wide" type="info">
创建后配置安全与发信
</el-tag>
<div class="security-edit-item">
<div>
<strong>安全码</strong>
<span v-if="recoveryCodeWebManaged">
{{ recoveryCodeMeta(selectedUser?.RecoveryCodeConfigured).label }}
</span>
<span v-else>部署端管理</span>
<span v-else-if="editModel === 'edit'">部署端管理</span>
</div>
<el-button
:disabled="!recoveryCodeWebManaged || submitting"
:loading="securityDialogOpening === 'recovery-code'"
@click="openRecoveryCodeDialog"
>
{{ recoveryCodeWebManaged ? '管理安全码' : '仅支持本机管理' }}
{{ recoveryCodeWebManaged || editModel === 'create' ? '管理安全码' : '仅支持本机管理' }}
</el-button>
</div>
<div class="security-edit-item">
@ -264,16 +267,23 @@
</span>
</div>
<el-button
:disabled="submitting"
:disabled="!selectedUser?.ID || submitting"
:loading="securityDialogOpening === 'external-email'"
@click="openExternalEmailDialog"
>
编辑外部邮箱
</el-button>
</div>
<div v-if="canManageNotification" class="security-edit-item account-form-wide">
<strong>系统通知发信</strong>
<el-button
:disabled="!selectedUser?.ID || submitting"
@click="notificationDialogVisible = true"
>管理发信授权</el-button>
</div>
</div>
<el-form-item>
<el-form-item class="account-form-wide">
<div class="status-switch">
<span class="switch-label">{{ lang.status }}</span>
<el-switch
@ -293,12 +303,28 @@
<span class="dialog-footer">
<el-button :disabled="submitting" @click="userInfoDialog = false">{{ lang.cancel }}</el-button>
<el-button type="primary" :disabled="!canEditSelectedUser" :loading="submitting" @click="submit">
{{ lang.confirm }}
{{ editModel === 'create' ? '创建并继续配置' : lang.confirm }}
</el-button>
</span>
</template>
</el-dialog>
<el-dialog
v-model="notificationDialogVisible"
title="系统通知发信"
width="min(760px, 94vw)"
append-to-body
destroy-on-close
:before-close="closeNotificationDialog"
>
<SystemNotification
v-if="notificationDialogVisible && selectedUser?.ID"
:key="selectedUser.ID"
ref="notificationPanel"
:user-id="selectedUser.ID"
/>
</el-dialog>
<RecoveryCodeDialog
v-model="recoveryCodeDialogVisible"
:user="selectedUser"
@ -317,6 +343,7 @@
import { usePage, Plus } from "./UserManagement.js";
import RecoveryCodeDialog from "./RecoveryCodeDialog/RecoveryCodeDialog.vue";
import UserExternalEmailDialog from "./UserExternalEmailDialog/UserExternalEmailDialog.vue";
import SystemNotification from "../SystemNotification/SystemNotification.vue";
const {
lang,
@ -358,6 +385,10 @@ const {
recoveryCodeWebManaged,
recoveryCodeDialogVisible,
externalEmailDialogVisible,
notificationDialogVisible,
notificationPanel,
canManageNotification,
closeNotificationDialog,
securityDialogOpening,
openRecoveryCodeDialog,
openExternalEmailDialog,