mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/1Panel-dev/1Panel.git
synced 2026-09-20 08:03:55 +08:00
fix(database): improve MySQL user display (#13344)
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -81,6 +81,7 @@ export namespace Database {
|
||||
password: string;
|
||||
host: string;
|
||||
description: string;
|
||||
dbs?: string[];
|
||||
}
|
||||
|
||||
export interface MysqlUserDelete {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user