fix(database): improve MySQL user display (#13344)

This commit is contained in:
ssongliu
2026-07-23 14:44:07 +08:00
committed by GitHub
parent 2a2e6607b8
commit 0c046edfe5
5 changed files with 195 additions and 68 deletions

View File

@@ -90,11 +90,12 @@ type MysqlUserSearch struct {
}
type MysqlUserCreate struct {
Database string `json:"database" validate:"required"`
Username string `json:"username" validate:"required"`
Password string `json:"password" validate:"required"`
Host string `json:"host" validate:"required"`
Description string `json:"description"`
Database string `json:"database" validate:"required"`
Username string `json:"username" validate:"required"`
Password string `json:"password" validate:"required"`
Host string `json:"host" validate:"required"`
Description string `json:"description"`
DBs []string `json:"dbs"`
}
type MysqlUserDelete struct {

View File

@@ -618,6 +618,24 @@ func (u *MysqlService) CreateUser(req dto.MysqlUserCreate) error {
if cmd.CheckIllegal(req.Username, req.Password, req.Host) {
return buserr.New("ErrCmdIllegal")
}
dbs := make([]string, 0, len(req.DBs))
dbSet := make(map[string]struct{}, len(req.DBs))
for _, db := range req.DBs {
if db == "" {
continue
}
if cmd.CheckIllegal(db) {
return buserr.New("ErrCmdIllegal")
}
if db == "*" {
return errors.New("global mysql privileges must be managed outside 1Panel")
}
if _, ok := dbSet[db]; ok {
continue
}
dbSet[db] = struct{}{}
dbs = append(dbs, db)
}
if err := checkMysqlNormalUser(req.Username); err != nil {
return err
}
@@ -643,6 +661,53 @@ func (u *MysqlService) CreateUser(req dto.MysqlUserCreate) error {
}
}
}
savedHosts := make([]string, 0, len(hosts))
rollbackSavedHosts := func() {
for i := len(savedHosts) - 1; i >= 0; i-- {
host := savedHosts[i]
if rollbackErr := databaseUserRepo.Delete(
repo.WithByType(dbType),
databaseUserRepo.WithByDatabase(req.Database),
databaseUserRepo.WithByUser(req.Username, host),
); rollbackErr != nil {
global.LOG.Errorf("rollback mysql user record %s@%s failed, err: %v", req.Username, host, rollbackErr)
}
}
}
grantedItems := make([]client.GrantInfo, 0, len(dbs)*len(hosts))
rollbackGrantedItems := func() {
for i := len(grantedItems) - 1; i >= 0; i-- {
item := grantedItems[i]
if rollbackErr := cli.RevokeGrant(item, 300); rollbackErr != nil {
global.LOG.Errorf(
"rollback mysql grant %s to %s@%s failed, err: %v",
item.Database,
item.Username,
item.Host,
rollbackErr,
)
}
if rollbackErr := databaseUserGrantRepo.Delete(
repo.WithByType(dbType),
databaseUserGrantRepo.WithByDatabase(req.Database),
databaseUserGrantRepo.WithByDBName(item.Database),
databaseUserGrantRepo.WithByUser(item.Username, item.Host),
); rollbackErr != nil {
global.LOG.Errorf(
"rollback mysql grant record %s to %s@%s failed, err: %v",
item.Database,
item.Username,
item.Host,
rollbackErr,
)
}
}
}
rollbackAll := func() {
rollbackGrantedItems()
rollbackSavedHosts()
rollbackCreatedHosts()
}
for _, host := range hosts {
if err := cli.CreateUserOnly(client.UserInfo{Username: req.Username, Host: host}, req.Password, 300); err != nil {
rollbackCreatedHosts()
@@ -650,23 +715,27 @@ func (u *MysqlService) CreateUser(req dto.MysqlUserCreate) error {
}
createdHosts = append(createdHosts, host)
}
savedHosts := make([]string, 0, len(hosts))
for _, host := range hosts {
if err := saveDatabaseUserCredential(dbType, req.Database, req.Username, host, req.Password, req.Description); err != nil {
for _, savedHost := range savedHosts {
if rollbackErr := databaseUserRepo.Delete(
repo.WithByType(dbType),
databaseUserRepo.WithByDatabase(req.Database),
databaseUserRepo.WithByUser(req.Username, savedHost),
); rollbackErr != nil {
global.LOG.Errorf("rollback mysql user record %s@%s failed, err: %v", req.Username, savedHost, rollbackErr)
}
}
rollbackCreatedHosts()
rollbackAll()
return err
}
savedHosts = append(savedHosts, host)
}
for _, db := range dbs {
for _, host := range hosts {
item := client.GrantInfo{Database: db, Username: req.Username, Host: host}
if err := cli.GrantUser(item, 300); err != nil {
rollbackAll()
return err
}
grantedItems = append(grantedItems, item)
if err := saveDatabaseUserGrant(dbType, req.Database, db, req.Username, host); err != nil {
rollbackAll()
return err
}
}
}
return nil
}

View File

@@ -81,6 +81,7 @@ export namespace Database {
password: string;
host: string;
description: string;
dbs?: string[];
}
export interface MysqlUserDelete {

View File

@@ -1,21 +1,25 @@
<template>
<DialogPro v-model="dialogVisible" :title="$t('database.authorizationManagement')" size="large">
<DialogPro
v-model="dialogVisible"
:title="$t(props.viewOnly ? 'database.authorizedUsers' : 'database.authorizationManagement')"
size="large"
>
<div v-loading="loading">
<div class="authorization-toolbar">
<div v-if="!props.viewOnly" class="authorization-toolbar">
<el-button type="primary" @click="openAddDialog">
{{ $t('database.addUserAuthorization') }}
</el-button>
</div>
<el-table :data="authorizedUsers" :empty-text="$t('commons.msg.noneData')">
<el-table-column :label="$t('commons.login.username')" min-width="140">
<template #default="{ row }">{{ row.username }}</template>
<el-table-column :label="$t('commons.table.user')" show-overflow-tooltip min-width="180">
<template #default="{ row }">{{ row.username }}@{{ row.host }}</template>
</el-table-column>
<el-table-column :label="$t('commons.login.password')" prop="password" min-width="180">
<template #default="{ row }">
<span v-if="!row.password">-</span>
<div v-else class="password-cell">
<span v-if="!row.showPassword" class="password-text">**********</span>
<span v-else class="password-text">{{ row.password }}</span>
<Tooltip v-else class="password-text" :islink="false" :text="row.password" />
<el-button
v-if="!row.showPassword"
link
@@ -34,16 +38,13 @@
</div>
</template>
</el-table-column>
<el-table-column :label="$t('database.permission')" min-width="110">
<template #default="{ row }">{{ permissionLabel(row.host) }}</template>
</el-table-column>
<el-table-column
:label="$t('commons.table.description')"
prop="description"
show-overflow-tooltip
min-width="120"
/>
<el-table-column :label="$t('commons.table.operate')" width="100" fixed="right">
<el-table-column v-if="!props.viewOnly" :label="$t('commons.table.operate')" width="100" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="revokeAuthorization(row)">
{{ $t('database.revokeAuthorization') }}
@@ -59,7 +60,12 @@
</template>
</DialogPro>
<DialogPro v-model="addDialogVisible" :title="$t('database.addUserAuthorization')" size="small">
<DialogPro
v-if="!props.viewOnly"
v-model="addDialogVisible"
:title="$t('database.addUserAuthorization')"
size="small"
>
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" v-loading="loading">
<el-form-item :label="$t('commons.table.type')" prop="mode">
<el-radio-group v-model="form.mode" @change="changeMode">
@@ -155,6 +161,7 @@
<script lang="ts" setup>
import { computed, reactive, ref } from 'vue';
import { ElMessageBox } from 'element-plus';
import Tooltip from '@/components/tooltip/index.vue';
import i18n from '@/lang';
import { Rules } from '@/global/form-rules';
import { MsgSuccess } from '@/utils/message';
@@ -168,6 +175,14 @@ import {
} from '@/api/modules/database';
const emit = defineEmits<{ (e: 'search'): void }>();
const props = withDefaults(
defineProps<{
viewOnly?: boolean;
}>(),
{
viewOnly: false,
},
);
const dialogVisible = ref(false);
const addDialogVisible = ref(false);

View File

@@ -1,20 +1,20 @@
<template>
<DrawerPro v-model="drawerVisible" :header="$t('commons.table.user')" size="70%">
<DrawerPro v-model="drawerVisible" :header="$t('commons.table.user')" size="60%">
<div class="drawer-toolbar">
<el-button type="primary" @click="openUserDialog()">
{{ $t('commons.button.create') }}
</el-button>
</div>
<ComplexTable :data="users" :heightDiff="260">
<el-table-column :label="$t('commons.login.username')" prop="username" min-width="150">
<el-table-column :label="$t('commons.table.user')" show-overflow-tooltip prop="username" min-width="180">
<template #default="{ row }">
<span>{{ row.username }}</span>
<span>{{ row.username }}@{{ row.host }}</span>
<el-tag v-if="row.isDelete" round type="info" class="ml-1" size="small">
{{ $t('database.isDelete') }}
</el-tag>
</template>
</el-table-column>
<el-table-column :label="$t('commons.login.password')" prop="password" min-width="180">
<el-table-column :label="$t('commons.login.password')" prop="password" min-width="120">
<template #default="{ row }">
<span v-if="row.isDelete">-</span>
<div v-else-if="!row.password" class="password-cell">
@@ -24,7 +24,7 @@
</div>
<div class="password-cell" v-else>
<span v-if="!row.showPassword" class="password-text">**********</span>
<span v-else class="password-text">{{ row.password }}</span>
<Tooltip v-else class="password-text" :islink="false" :text="row.password" />
<el-button
v-if="!row.showPassword"
link
@@ -43,18 +43,35 @@
</div>
</template>
</el-table-column>
<el-table-column :label="$t('database.permission')" show-overflow-tooltip prop="host" min-width="120">
<template #default="{ row }">
{{ permissionLabel(row.host) }}
</template>
</el-table-column>
<el-table-column :label="$t('menu.database')" min-width="180">
<el-table-column :label="$t('menu.database')" min-width="160">
<template #default="{ row }">
<span v-if="userDatabases(row).length === 0">-</span>
<div v-else class="bind-db-list">
<el-tag v-for="item in userDatabases(row)" :key="row.username + row.host + item" size="small">
{{ item }}
</el-tag>
<el-tooltip
v-for="item in userDatabases(row).slice(0, 1)"
:key="row.username + '@' + row.host + item"
:content="item"
placement="top"
>
<el-tag size="small">{{ item }}</el-tag>
</el-tooltip>
<el-popover v-if="userDatabases(row).length > 1" placement="right" trigger="click" :width="260">
<template #reference>
<el-tag class="cursor-pointer" type="info" size="small">
+{{ userDatabases(row).length - 1 }}
</el-tag>
</template>
<div class="bind-db-popover">
<el-tooltip
v-for="item in userDatabases(row)"
:key="row.username + '@' + row.host + item"
:content="item"
placement="top"
>
<el-tag size="small">{{ item }}</el-tag>
</el-tooltip>
</div>
</el-popover>
</div>
</template>
</el-table-column>
@@ -62,10 +79,10 @@
:label="$t('commons.table.description')"
prop="description"
show-overflow-tooltip
min-width="160"
min-width="120"
/>
<fu-table-operations
:min-width="100"
:width="120"
:buttons="userButtons"
:label="$t('commons.table.operate')"
fixed="right"
@@ -100,7 +117,7 @@
<el-form-item :label="$t('commons.table.description')" prop="description">
<el-input type="textarea" clearable v-model="userForm.description" />
</el-form-item>
<el-form-item v-if="userDialogMode === 'edit'" :label="$t('menu.database')" prop="dbs">
<el-form-item :label="$t('menu.database')" prop="dbs">
<el-select v-model="userForm.dbs" filterable multiple collapse-tags-tooltip>
<el-option v-for="item in databases" :key="item.name" :label="item.name" :value="item.name" />
</el-select>
@@ -185,6 +202,7 @@
<script lang="ts" setup>
import { computed, reactive, ref } from 'vue';
import Tooltip from '@/components/tooltip/index.vue';
import i18n from '@/lang';
import { Rules } from '@/global/form-rules';
import { MsgSuccess } from '@/utils/message';
@@ -337,18 +355,27 @@ const loadContext = async () => {
await Promise.all([loadUsers(), loadGrants(), loadDatabases()]);
};
const userDatabases = (row: Database.MysqlUser) => {
return Array.from(
new Set(
grants.value
.filter((item) => item.username === row.username && item.host === row.host)
.map((item) => item.database),
),
);
};
const userDatabaseMap = computed(() => {
const databaseSets = new Map<string, Set<string>>();
for (const grant of grants.value) {
const key = `${grant.username}@${grant.host}`;
let databases = databaseSets.get(key);
if (!databases) {
databases = new Set<string>();
databaseSets.set(key, databases);
}
databases.add(grant.database);
}
const permissionLabel = (host: string) => {
return host === '%' ? i18n.global.t('database.permissionAll') : host;
const result = new Map<string, string[]>();
for (const [key, databases] of databaseSets) {
result.set(key, Array.from(databases));
}
return result;
});
const userDatabases = (row: Database.MysqlUser) => {
return userDatabaseMap.value.get(`${row.username}@${row.host}`) || [];
};
const changePermission = () => {
@@ -477,21 +504,22 @@ const submitUser = async () => {
}
return;
}
await createMysqlUser({
database: database.value,
username: userForm.username,
host: userForm.host,
password: userForm.password,
description: userForm.description,
})
.then(() => {
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
userDialogVisible.value = false;
loadUsers();
})
.finally(() => {
loading.value = false;
try {
await createMysqlUser({
database: database.value,
username: userForm.username,
host: userForm.host,
password: userForm.password,
description: userForm.description,
dbs: userForm.dbs,
});
await loadContext();
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
userDialogVisible.value = false;
emit('search');
} finally {
loading.value = false;
}
});
};
@@ -566,6 +594,19 @@ defineExpose({
flex-wrap: wrap;
gap: 6px;
}
.bind-db-popover {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
:deep(.el-tag) {
width: 100%;
justify-content: flex-start;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.delete-user-title {
margin-bottom: 12px;
font-size: 14px;