mirror of
https://github.com/acepanel/panel.git
synced 2026-02-04 03:07:20 +08:00
feat: 数据库用户管理2
This commit is contained in:
@@ -9,6 +9,7 @@ type DatabaseStatus string
|
||||
type Database struct {
|
||||
Name string `json:"name"`
|
||||
Server string `json:"server"`
|
||||
ServerID uint `json:"server_id"`
|
||||
Encoding string `json:"encoding"`
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ func (r databaseRepo) List(page, limit uint) ([]*biz.Database, int64, error) {
|
||||
database = append(database, &biz.Database{
|
||||
Name: item.Name,
|
||||
Server: server.Name,
|
||||
ServerID: server.ID,
|
||||
Encoding: item.CharSet,
|
||||
})
|
||||
}
|
||||
@@ -48,6 +49,7 @@ func (r databaseRepo) List(page, limit uint) ([]*biz.Database, int64, error) {
|
||||
database = append(database, &biz.Database{
|
||||
Name: item.Name,
|
||||
Server: server.Name,
|
||||
ServerID: server.ID,
|
||||
Encoding: item.Encoding,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package data
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"slices"
|
||||
|
||||
"github.com/samber/do/v2"
|
||||
@@ -121,14 +122,16 @@ func (r databaseServerRepo) Sync(id uint) error {
|
||||
for user := range slices.Values(allUsers) {
|
||||
if !slices.ContainsFunc(users, func(a *biz.DatabaseUser) bool {
|
||||
return a.Username == user.User && a.Host == user.Host
|
||||
}) {
|
||||
}) && !slices.Contains([]string{"root", "mysql.sys", "mysql.session", "mysql.infoschema"}, user.User) {
|
||||
newUser := &biz.DatabaseUser{
|
||||
ServerID: id,
|
||||
Username: user.User,
|
||||
Host: user.Host,
|
||||
Remark: fmt.Sprintf("sync from server %s", server.Name),
|
||||
}
|
||||
app.Orm.Create(newUser)
|
||||
if err = app.Orm.Create(newUser).Error; err != nil {
|
||||
app.Logger.Warn("sync database user failed", slog.Any("err", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
case biz.DatabaseTypePostgresql:
|
||||
@@ -143,7 +146,7 @@ func (r databaseServerRepo) Sync(id uint) error {
|
||||
for user := range slices.Values(allUsers) {
|
||||
if !slices.ContainsFunc(users, func(a *biz.DatabaseUser) bool {
|
||||
return a.Username == user.Role
|
||||
}) {
|
||||
}) && !slices.Contains([]string{"postgres"}, user.Role) {
|
||||
newUser := &biz.DatabaseUser{
|
||||
ServerID: id,
|
||||
Username: user.Role,
|
||||
|
||||
@@ -66,7 +66,7 @@ func Http(r chi.Router) {
|
||||
database := service.NewDatabaseService()
|
||||
r.Get("/", database.List)
|
||||
r.Post("/", database.Create)
|
||||
r.Delete("/{id}", database.Delete)
|
||||
r.Delete("/", database.Delete)
|
||||
})
|
||||
|
||||
r.Route("/databaseServer", func(r chi.Router) {
|
||||
@@ -75,7 +75,7 @@ func Http(r chi.Router) {
|
||||
r.Post("/", server.Create)
|
||||
r.Put("/{id}", server.Update)
|
||||
r.Delete("/{id}", server.Delete)
|
||||
r.Delete("/{id}/sync", server.Sync)
|
||||
r.Post("/{id}/sync", server.Sync)
|
||||
})
|
||||
|
||||
r.Route("/databaseUser", func(r chi.Router) {
|
||||
|
||||
@@ -5,10 +5,8 @@ export default {
|
||||
list: (page: number, limit: number) => http.Get(`/database`, { params: { page, limit } }),
|
||||
// 创建数据库
|
||||
create: (data: any) => http.Post(`/database`, data),
|
||||
// 更新数据库
|
||||
update: (id: number, data: any) => http.Put(`/database/${id}`, data),
|
||||
// 删除数据库
|
||||
delete: (id: number) => http.Delete(`/database/${id}`),
|
||||
delete: (server_id: number, name: string) => http.Delete(`/database`, { server_id, name }),
|
||||
// 获取数据库服务器列表
|
||||
serverList: (page: number, limit: number) =>
|
||||
http.Get('/databaseServer', { params: { page, limit } }),
|
||||
|
||||
110
web/src/views/database/CreateUserModal.vue
Normal file
110
web/src/views/database/CreateUserModal.vue
Normal file
@@ -0,0 +1,110 @@
|
||||
<script setup lang="ts">
|
||||
import database from '@/api/panel/database'
|
||||
import { NButton, NInput } from 'naive-ui'
|
||||
|
||||
const show = defineModel<boolean>('show', { type: Boolean, required: true })
|
||||
const createModel = ref({
|
||||
name: '',
|
||||
type: 'mysql',
|
||||
host: '127.0.0.1',
|
||||
port: 3306,
|
||||
username: '',
|
||||
password: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const databaseType = [
|
||||
{ label: 'MySQL', value: 'mysql' },
|
||||
{ label: 'PostgreSQL', value: 'postgresql' }
|
||||
]
|
||||
|
||||
const handleCreate = () => {
|
||||
useRequest(() => database.serverCreate(createModel.value)).onSuccess(() => {
|
||||
show.value = false
|
||||
window.$message.success('添加成功')
|
||||
window.$bus.emit('database:refresh')
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-modal
|
||||
v-model:show="show"
|
||||
preset="card"
|
||||
title="添加数据库服务器"
|
||||
style="width: 60vw"
|
||||
size="huge"
|
||||
:bordered="false"
|
||||
:segmented="false"
|
||||
@close="show = false"
|
||||
>
|
||||
<n-form :model="createModel">
|
||||
<n-form-item path="name" label="名称">
|
||||
<n-input
|
||||
v-model:value="createModel.name"
|
||||
type="text"
|
||||
@keydown.enter.prevent
|
||||
placeholder="输入数据库服务器名称"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item path="type" label="类型">
|
||||
<n-select
|
||||
v-model:value="createModel.type"
|
||||
@keydown.enter.prevent
|
||||
placeholder="选择数据库类型"
|
||||
:options="databaseType"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-row :gutter="[0, 24]">
|
||||
<n-col :span="15">
|
||||
<n-form-item path="host" label="主机">
|
||||
<n-input
|
||||
v-model:value="createModel.host"
|
||||
type="text"
|
||||
@keydown.enter.prevent
|
||||
placeholder="输入数据库服务器主机"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-col>
|
||||
<n-col :span="2"></n-col>
|
||||
<n-col :span="7">
|
||||
<n-form-item path="port" label="端口">
|
||||
<n-input-number
|
||||
w-full
|
||||
v-model:value="createModel.port"
|
||||
@keydown.enter.prevent
|
||||
placeholder="输入数据库服务器端口"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-col>
|
||||
</n-row>
|
||||
<n-form-item path="username" label="用户名">
|
||||
<n-input
|
||||
v-model:value="createModel.username"
|
||||
type="text"
|
||||
@keydown.enter.prevent
|
||||
placeholder="输入数据库服务器用户名"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item path="password" label="密码">
|
||||
<n-input
|
||||
v-model:value="createModel.password"
|
||||
type="password"
|
||||
@keydown.enter.prevent
|
||||
placeholder="输入数据库服务器密码"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item path="remark" label="备注">
|
||||
<n-input
|
||||
v-model:value="createModel.remark"
|
||||
type="textarea"
|
||||
@keydown.enter.prevent
|
||||
placeholder="输入数据库服务器备注"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<n-button type="info" block @click="handleCreate">提交</n-button>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -1,86 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import { renderIcon } from '@/utils'
|
||||
import { NButton, NInput, NInputGroup, NPopconfirm, NTag } from 'naive-ui'
|
||||
import { NButton, NPopconfirm, NTag } from 'naive-ui'
|
||||
|
||||
import database from '@/api/panel/database'
|
||||
import { formatDateTime } from '@/utils'
|
||||
|
||||
const columns: any = [
|
||||
{
|
||||
title: '名称',
|
||||
title: '数据库名',
|
||||
key: 'name',
|
||||
minWidth: 100,
|
||||
resizable: true,
|
||||
ellipsis: { tooltip: true }
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
key: 'type',
|
||||
width: 150,
|
||||
render(row: any) {
|
||||
return h(
|
||||
NTag,
|
||||
{ type: 'info' },
|
||||
{
|
||||
default: () => {
|
||||
switch (row.type) {
|
||||
case 'mysql':
|
||||
return 'MySQL'
|
||||
case 'postgresql':
|
||||
return 'PostgreSQL'
|
||||
default:
|
||||
return row.type
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
title: '服务器',
|
||||
key: 'server',
|
||||
width: 300
|
||||
},
|
||||
{
|
||||
title: '用户名',
|
||||
key: 'username',
|
||||
width: 150,
|
||||
ellipsis: { tooltip: true },
|
||||
render(row: any) {
|
||||
return row.username || '无'
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '密码',
|
||||
key: 'password',
|
||||
width: 200,
|
||||
render(row: any) {
|
||||
return h(NInputGroup, null, {
|
||||
default: () => [
|
||||
h(NInput, {
|
||||
value: row.password,
|
||||
type: 'password',
|
||||
showPasswordOn: 'click',
|
||||
readonly: true
|
||||
})
|
||||
]
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '主机',
|
||||
key: 'host',
|
||||
title: '编码',
|
||||
key: 'encoding',
|
||||
width: 200,
|
||||
render(row: any) {
|
||||
return h(NTag, null, {
|
||||
default: () => `${row.host}:${row.port}`
|
||||
default: () => row.encoding
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '更新日期',
|
||||
key: 'updated_at',
|
||||
width: 200,
|
||||
ellipsis: { tooltip: true },
|
||||
render(row: any) {
|
||||
return formatDateTime(row.updated_at)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
@@ -92,11 +38,11 @@ const columns: any = [
|
||||
h(
|
||||
NPopconfirm,
|
||||
{
|
||||
onPositiveClick: () => handleDelete(row.id)
|
||||
onPositiveClick: () => handleDelete(row.server_id, row.name)
|
||||
},
|
||||
{
|
||||
default: () => {
|
||||
return '确定删除服务器吗?'
|
||||
return '确定删除数据库吗?'
|
||||
},
|
||||
trigger: () => {
|
||||
return h(
|
||||
@@ -128,8 +74,8 @@ const { loading, data, page, total, pageSize, pageCount, refresh } = usePaginati
|
||||
}
|
||||
)
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
await database.delete(id).then(() => {
|
||||
const handleDelete = async (serverID: number, name: string) => {
|
||||
await database.delete(serverID, name).then(() => {
|
||||
window.$message.success('删除成功')
|
||||
refresh()
|
||||
})
|
||||
|
||||
@@ -4,15 +4,17 @@ defineOptions({
|
||||
})
|
||||
|
||||
import CreateDatabaseModal from '@/views/database/CreateDatabaseModal.vue'
|
||||
import CreateDatabaseServerModal from '@/views/database/CreateServerModal.vue'
|
||||
import DatabaseListView from '@/views/database/DatabaseList.vue'
|
||||
import ServerListView from '@/views/database/ServerList.vue'
|
||||
import CreateServerModal from '@/views/database/CreateServerModal.vue'
|
||||
import DatabaseList from '@/views/database/DatabaseList.vue'
|
||||
import ServerList from '@/views/database/ServerList.vue'
|
||||
import UserList from '@/views/database/UserList.vue'
|
||||
import { NButton } from 'naive-ui'
|
||||
|
||||
const currentTab = ref('database')
|
||||
|
||||
const createDatabaseModalShow = ref(false)
|
||||
const createDatabaseServerModalShow = ref(false)
|
||||
const createUserModalShow = ref(false)
|
||||
const createServerModalShow = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -26,28 +28,30 @@ const createDatabaseServerModalShow = ref(false)
|
||||
<TheIcon :size="18" icon="material-symbols:add" />
|
||||
创建数据库
|
||||
</n-button>
|
||||
<n-flex v-if="currentTab === 'server'">
|
||||
<n-button type="success">
|
||||
<TheIcon :size="18" icon="material-symbols:sync" />
|
||||
同步数据库
|
||||
</n-button>
|
||||
<n-button type="primary" @click="createDatabaseServerModalShow = true">
|
||||
<TheIcon :size="18" icon="material-symbols:add" />
|
||||
添加服务器
|
||||
</n-button>
|
||||
</n-flex>
|
||||
<n-button v-if="currentTab === 'user'" type="primary" @click="createUserModalShow = true">
|
||||
<TheIcon :size="18" icon="material-symbols:add" />
|
||||
创建用户
|
||||
</n-button>
|
||||
<n-button v-if="currentTab === 'server'" type="primary" @click="createServerModalShow = true">
|
||||
<TheIcon :size="18" icon="material-symbols:add" />
|
||||
添加服务器
|
||||
</n-button>
|
||||
</template>
|
||||
<n-flex vertical>
|
||||
<n-tabs v-model:value="currentTab" type="line" animated>
|
||||
<n-tab-pane name="database" tab="数据库">
|
||||
<database-list-view v-model:type="currentTab" />
|
||||
<database-list v-model:type="currentTab" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="user" tab="用户">
|
||||
<user-list v-model:type="currentTab" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="server" tab="服务器">
|
||||
<server-list-view v-model:type="currentTab" />
|
||||
<server-list v-model:type="currentTab" />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</n-flex>
|
||||
</common-page>
|
||||
<create-database-modal v-model:show="createDatabaseModalShow" />
|
||||
<create-database-server-modal v-model:show="createDatabaseServerModalShow" />
|
||||
<create-user-modal v-model:show="createUserModalShow" />
|
||||
<create-server-modal v-model:show="createServerModalShow" />
|
||||
</template>
|
||||
|
||||
@@ -56,7 +56,8 @@ const columns: any = [
|
||||
value: row.password,
|
||||
type: 'password',
|
||||
showPasswordOn: 'click',
|
||||
readonly: true
|
||||
readonly: true,
|
||||
placeholder: '无'
|
||||
})
|
||||
]
|
||||
})
|
||||
@@ -89,6 +90,35 @@ const columns: any = [
|
||||
hideInExcel: true,
|
||||
render(row: any) {
|
||||
return [
|
||||
h(
|
||||
NPopconfirm,
|
||||
{
|
||||
onPositiveClick: async () => {
|
||||
await database.serverSync(row.id).then(() => {
|
||||
window.$message.success('同步成功')
|
||||
refresh()
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
default: () => {
|
||||
return '确定同步数据库用户(不包括密码)到面板?'
|
||||
},
|
||||
trigger: () => {
|
||||
return h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'success'
|
||||
},
|
||||
{
|
||||
default: () => '同步',
|
||||
icon: renderIcon('material-symbols:sync', { size: 14 })
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
),
|
||||
h(
|
||||
NPopconfirm,
|
||||
{
|
||||
@@ -136,13 +166,13 @@ const handleDelete = async (id: number) => {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.$bus.on('database:refresh', () => {
|
||||
window.$bus.on('database-server:refresh', () => {
|
||||
refresh()
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.$bus.off('database:refresh')
|
||||
window.$bus.off('database-server:refresh')
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,9 +1,160 @@
|
||||
<script setup lang="ts"></script>
|
||||
<script setup lang="ts">
|
||||
import { renderIcon } from '@/utils'
|
||||
import { NButton, NInput, NInputGroup, NPopconfirm, NTag } from 'naive-ui'
|
||||
|
||||
import database from '@/api/panel/database'
|
||||
import { formatDateTime } from '@/utils'
|
||||
|
||||
const columns: any = [
|
||||
{
|
||||
title: '用户名',
|
||||
key: 'username',
|
||||
minWidth: 100,
|
||||
resizable: true,
|
||||
ellipsis: { tooltip: true },
|
||||
render(row: any) {
|
||||
return row.username || '无'
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '密码',
|
||||
key: 'password',
|
||||
width: 200,
|
||||
render(row: any) {
|
||||
return h(NInputGroup, null, {
|
||||
default: () => [
|
||||
h(NInput, {
|
||||
value: row.password,
|
||||
type: 'password',
|
||||
showPasswordOn: 'click',
|
||||
readonly: true
|
||||
})
|
||||
]
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '主机',
|
||||
key: 'host',
|
||||
width: 200,
|
||||
render(row: any) {
|
||||
return h(NTag, null, {
|
||||
default: () => row.host
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
key: 'remark',
|
||||
minWidth: 250,
|
||||
resizable: true,
|
||||
ellipsis: { tooltip: true },
|
||||
render(row: any) {
|
||||
return h(NInput, {
|
||||
size: 'small',
|
||||
value: row.remark
|
||||
/*onBlur: () => handleRemark(row),
|
||||
onUpdateValue(v) {
|
||||
row.remark = v
|
||||
}*/
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '更新日期',
|
||||
key: 'updated_at',
|
||||
width: 200,
|
||||
ellipsis: { tooltip: true },
|
||||
render(row: any) {
|
||||
return formatDateTime(row.updated_at)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 200,
|
||||
align: 'center',
|
||||
hideInExcel: true,
|
||||
render(row: any) {
|
||||
return [
|
||||
h(
|
||||
NPopconfirm,
|
||||
{
|
||||
onPositiveClick: () => handleDelete(row.id)
|
||||
},
|
||||
{
|
||||
default: () => {
|
||||
return '确定删除服务器吗?'
|
||||
},
|
||||
trigger: () => {
|
||||
return h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'error',
|
||||
style: 'margin-left: 15px;'
|
||||
},
|
||||
{
|
||||
default: () => '删除',
|
||||
icon: renderIcon('material-symbols:delete-outline', { size: 14 })
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const { loading, data, page, total, pageSize, pageCount, refresh } = usePagination(
|
||||
(page, pageSize) => database.userList(page, pageSize),
|
||||
{
|
||||
initialData: { total: 0, list: [] },
|
||||
total: (res: any) => res.total,
|
||||
data: (res: any) => res.items
|
||||
}
|
||||
)
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
await database.userDelete(id).then(() => {
|
||||
window.$message.success('删除成功')
|
||||
refresh()
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.$bus.on('database-user:refresh', () => {
|
||||
refresh()
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.$bus.off('database-user:refresh')
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h1>UserList</h1>
|
||||
</div>
|
||||
<n-data-table
|
||||
striped
|
||||
remote
|
||||
:scroll-x="1200"
|
||||
:loading="loading"
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
:row-key="(row: any) => row.name"
|
||||
v-model:page="page"
|
||||
v-model:pageSize="pageSize"
|
||||
:pagination="{
|
||||
page: page,
|
||||
pageCount: pageCount,
|
||||
pageSize: pageSize,
|
||||
itemCount: total,
|
||||
showQuickJumper: true,
|
||||
showSizePicker: true,
|
||||
pageSizes: [20, 50, 100, 200]
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
Reference in New Issue
Block a user