From 33484a94364d5f5c511db172c79318945d58ad98 Mon Sep 17 00:00:00 2001
From: ssongliu <73214554+ssongliu@users.noreply.github.com>
Date: Mon, 25 Dec 2023 17:08:09 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E5=90=8C=E6=AD=A5=E7=94=A8=E6=88=B7?=
 =?UTF-8?q?=E6=97=B6=EF=BC=8C=E7=A7=BB=E9=99=A4=E5=88=9B=E5=BB=BA=E7=94=A8?=
 =?UTF-8?q?=E6=88=B7=E9=80=BB=E8=BE=91=20(#3442)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 backend/app/api/v1/database_mysql.go          |  31 ++++
 backend/app/dto/database.go                   |   8 +
 backend/app/service/database_mysql.go         |  37 +++++
 backend/router/ro_database.go                 |   1 +
 backend/utils/mysql/client.go                 |   1 +
 backend/utils/mysql/client/info.go            |  24 ---
 backend/utils/mysql/client/local.go           |  13 --
 backend/utils/mysql/client/remote.go          |  13 --
 cmd/server/docs/docs.go                       |  72 ++++++++-
 cmd/server/docs/swagger.json                  |  72 ++++++++-
 cmd/server/docs/swagger.yaml                  |  49 +++++-
 frontend/src/api/interface/database.ts        |   7 +
 frontend/src/api/modules/database.ts          |   7 +
 frontend/src/lang/modules/en.ts               |   1 +
 frontend/src/lang/modules/tw.ts               |   1 +
 frontend/src/lang/modules/zh.ts               |   1 +
 .../src/views/database/mysql/bind/index.vue   | 139 ++++++++++++++++++
 frontend/src/views/database/mysql/index.vue   |  40 ++++-
 18 files changed, 458 insertions(+), 59 deletions(-)
 create mode 100644 frontend/src/views/database/mysql/bind/index.vue

diff --git a/backend/app/api/v1/database_mysql.go b/backend/app/api/v1/database_mysql.go
index a3aec9b16..b6c5f9282 100644
--- a/backend/app/api/v1/database_mysql.go
+++ b/backend/app/api/v1/database_mysql.go
@@ -41,6 +41,37 @@ func (b *BaseApi) CreateMysql(c *gin.Context) {
 	helper.SuccessWithData(c, nil)
 }
 
+// @Tags Database Mysql
+// @Summary Bind user of mysql database
+// @Description 绑定 mysql 数据库用户
+// @Accept json
+// @Param request body dto.BindUser true "request"
+// @Success 200
+// @Security ApiKeyAuth
+// @Router /databases/bind [post]
+// @x-panel-log {"bodyKeys":["database", "username"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"绑定 mysql 数据库名 [database] [username]","formatEN":"bind mysql database [database] [username]"}
+func (b *BaseApi) BindUser(c *gin.Context) {
+	var req dto.BindUser
+	if err := helper.CheckBindAndValidate(&req, c); err != nil {
+		return
+	}
+
+	if len(req.Password) != 0 {
+		password, err := base64.StdEncoding.DecodeString(req.Password)
+		if err != nil {
+			helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
+			return
+		}
+		req.Password = string(password)
+	}
+
+	if err := mysqlService.BindUser(req); err != nil {
+		helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
+		return
+	}
+	helper.SuccessWithData(c, nil)
+}
+
 // @Tags Database Mysql
 // @Summary Update mysql database description
 // @Description 更新 mysql 数据库库描述信息
diff --git a/backend/app/dto/database.go b/backend/app/dto/database.go
index cdf1966df..65aaa0ab4 100644
--- a/backend/app/dto/database.go
+++ b/backend/app/dto/database.go
@@ -43,6 +43,14 @@ type MysqlDBCreate struct {
 	Description string `json:"description"`
 }
 
+type BindUser struct {
+	Database   string `json:"database" validate:"required"`
+	DB         string `json:"db" validate:"required"`
+	Username   string `json:"username" validate:"required"`
+	Password   string `json:"password" validate:"required"`
+	Permission string `json:"permission" validate:"required"`
+}
+
 type MysqlLoadDB struct {
 	From     string `json:"from" validate:"required,oneof=local remote"`
 	Type     string `json:"type" validate:"required,oneof=mysql mariadb"`
diff --git a/backend/app/service/database_mysql.go b/backend/app/service/database_mysql.go
index cf61f1241..fe293bc9e 100644
--- a/backend/app/service/database_mysql.go
+++ b/backend/app/service/database_mysql.go
@@ -35,6 +35,7 @@ type IMysqlService interface {
 	SearchWithPage(search dto.MysqlDBSearch) (int64, interface{}, error)
 	ListDBOption() ([]dto.MysqlOption, error)
 	Create(ctx context.Context, req dto.MysqlDBCreate) (*model.DatabaseMysql, error)
+	BindUser(req dto.BindUser) error
 	LoadFromRemote(req dto.MysqlLoadDB) error
 	ChangeAccess(info dto.ChangeDBInfo) error
 	ChangePassword(info dto.ChangeDBInfo) error
@@ -144,6 +145,42 @@ func (u *MysqlService) Create(ctx context.Context, req dto.MysqlDBCreate) (*mode
 	return &createItem, nil
 }
 
+func (u *MysqlService) BindUser(req dto.BindUser) error {
+	dbItem, err := mysqlRepo.Get(mysqlRepo.WithByMysqlName(req.Database), commonRepo.WithByName(req.DB))
+	if err != nil {
+		return err
+	}
+	cli, version, err := LoadMysqlClientByFrom(req.Database)
+	if err != nil {
+		return err
+	}
+	defer cli.Close()
+
+	if err := cli.CreateUser(client.CreateInfo{
+		Name:       dbItem.Name,
+		Format:     dbItem.Format,
+		Username:   req.Username,
+		Password:   req.Password,
+		Permission: req.Permission,
+		Version:    version,
+		Timeout:    300,
+	}, false); err != nil {
+		return err
+	}
+	pass, err := encrypt.StringEncrypt(req.Password)
+	if err != nil {
+		return fmt.Errorf("decrypt database db password failed, err: %v", err)
+	}
+	if err := mysqlRepo.Update(dbItem.ID, map[string]interface{}{
+		"username":   req.Username,
+		"password":   pass,
+		"permission": req.Permission,
+	}); err != nil {
+		return err
+	}
+	return nil
+}
+
 func (u *MysqlService) LoadFromRemote(req dto.MysqlLoadDB) error {
 	client, version, err := LoadMysqlClientByFrom(req.Database)
 	if err != nil {
diff --git a/backend/router/ro_database.go b/backend/router/ro_database.go
index d62e4f283..88fa214f4 100644
--- a/backend/router/ro_database.go
+++ b/backend/router/ro_database.go
@@ -17,6 +17,7 @@ func (s *DatabaseRouter) InitRouter(Router *gin.RouterGroup) {
 	baseApi := v1.ApiGroupApp.BaseApi
 	{
 		cmdRouter.POST("", baseApi.CreateMysql)
+		cmdRouter.POST("/bind", baseApi.BindUser)
 		cmdRouter.POST("load", baseApi.LoadDBFromRemote)
 		cmdRouter.POST("/change/access", baseApi.ChangeMysqlAccess)
 		cmdRouter.POST("/change/password", baseApi.ChangeMysqlPassword)
diff --git a/backend/utils/mysql/client.go b/backend/utils/mysql/client.go
index 7af24ff06..14225adf9 100644
--- a/backend/utils/mysql/client.go
+++ b/backend/utils/mysql/client.go
@@ -14,6 +14,7 @@ import (
 
 type MysqlClient interface {
 	Create(info client.CreateInfo) error
+	CreateUser(info client.CreateInfo, withDeleteDB bool) error
 	Delete(info client.DeleteInfo) error
 
 	ChangePassword(info client.PasswordChangeInfo) error
diff --git a/backend/utils/mysql/client/info.go b/backend/utils/mysql/client/info.go
index 58455adcc..0c05951b6 100644
--- a/backend/utils/mysql/client/info.go
+++ b/backend/utils/mysql/client/info.go
@@ -4,9 +4,7 @@ import (
 	"crypto/tls"
 	"crypto/x509"
 	"errors"
-	"strings"
 
-	"github.com/1Panel-dev/1Panel/backend/utils/common"
 	"github.com/go-sql-driver/mysql"
 )
 
@@ -103,28 +101,6 @@ var formatMap = map[string]string{
 	"big5":    "big5_chinese_ci",
 }
 
-func loadNameByDB(name, version string) string {
-	nameItem := common.ConvertToPinyin(name)
-	if strings.HasPrefix(version, "5.6") {
-		if len(nameItem) <= 16 {
-			return nameItem
-		}
-		return strings.TrimSuffix(nameItem[:10], "_") + "_" + common.RandStr(5)
-	}
-	if len(nameItem) <= 32 {
-		return nameItem
-	}
-	return strings.TrimSuffix(nameItem[:25], "_") + "_" + common.RandStr(5)
-}
-
-func randomPassword(user string) string {
-	passwdItem := user
-	if len(user) > 6 {
-		passwdItem = user[:6]
-	}
-	return passwdItem + "@" + common.RandStrAndNum(8)
-}
-
 func VerifyPeerCertFunc(pool *x509.CertPool) func([][]byte, [][]*x509.Certificate) error {
 	return func(rawCerts [][]byte, _ [][]*x509.Certificate) error {
 		if len(rawCerts) == 0 {
diff --git a/backend/utils/mysql/client/local.go b/backend/utils/mysql/client/local.go
index 8c0b7a52c..96711b382 100644
--- a/backend/utils/mysql/client/local.go
+++ b/backend/utils/mysql/client/local.go
@@ -307,19 +307,6 @@ func (r *Local) SyncDB(version string) ([]SyncDBInfo, error) {
 			}
 		}
 		if len(dataItem.Username) == 0 {
-			dataItem.Username = loadNameByDB(parts[0], version)
-			dataItem.Password = randomPassword(dataItem.Username)
-			if err := r.CreateUser(CreateInfo{
-				Name:       parts[0],
-				Format:     parts[1],
-				Version:    version,
-				Username:   dataItem.Username,
-				Password:   dataItem.Password,
-				Permission: "%",
-				Timeout:    300,
-			}, false); err != nil {
-				global.LOG.Errorf("sync from remote server failed, err: create user failed %v", err)
-			}
 			dataItem.Permission = "%"
 		} else {
 			if isLocal {
diff --git a/backend/utils/mysql/client/remote.go b/backend/utils/mysql/client/remote.go
index 18a0b01f7..d385b2b47 100644
--- a/backend/utils/mysql/client/remote.go
+++ b/backend/utils/mysql/client/remote.go
@@ -333,19 +333,6 @@ func (r *Remote) SyncDB(version string) ([]SyncDBInfo, error) {
 			i++
 		}
 		if len(dataItem.Username) == 0 {
-			dataItem.Username = loadNameByDB(dbName, version)
-			dataItem.Password = randomPassword(dataItem.Username)
-			if err := r.CreateUser(CreateInfo{
-				Name:       dbName,
-				Format:     charsetName,
-				Version:    version,
-				Username:   dataItem.Username,
-				Password:   dataItem.Password,
-				Permission: "%",
-				Timeout:    300,
-			}, false); err != nil {
-				return datas, fmt.Errorf("sync db from remote server failed, err: create user failed %v", err)
-			}
 			dataItem.Permission = "%"
 		} else {
 			if isLocal {
diff --git a/cmd/server/docs/docs.go b/cmd/server/docs/docs.go
index 67f05d522..d288e5828 100644
--- a/cmd/server/docs/docs.go
+++ b/cmd/server/docs/docs.go
@@ -3952,6 +3952,49 @@ const docTemplate = `{
                 }
             }
         },
+        "/databases/bind": {
+            "post": {
+                "security": [
+                    {
+                        "ApiKeyAuth": []
+                    }
+                ],
+                "description": "绑定 mysql 数据库用户",
+                "consumes": [
+                    "application/json"
+                ],
+                "tags": [
+                    "Database Mysql"
+                ],
+                "summary": "Bind user of mysql database",
+                "parameters": [
+                    {
+                        "description": "request",
+                        "name": "request",
+                        "in": "body",
+                        "required": true,
+                        "schema": {
+                            "$ref": "#/definitions/dto.BindUser"
+                        }
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK"
+                    }
+                },
+                "x-panel-log": {
+                    "BeforeFunctions": [],
+                    "bodyKeys": [
+                        "database",
+                        "username"
+                    ],
+                    "formatEN": "bind mysql database [database] [username]",
+                    "formatZH": "绑定 mysql 数据库名 [database] [username]",
+                    "paramKeys": []
+                }
+            }
+        },
         "/databases/change/access": {
             "post": {
                 "security": [
@@ -10013,7 +10056,7 @@ const docTemplate = `{
                     "bodyKeys": [
                         "version"
                     ],
-                    "formatEN": "upgrade service =\u003e [version]",
+                    "formatEN": "upgrade system =\u003e [version]",
                     "formatZH": "更新系统 =\u003e [version]",
                     "paramKeys": []
                 }
@@ -13601,6 +13644,33 @@ const docTemplate = `{
                 }
             }
         },
+        "dto.BindUser": {
+            "type": "object",
+            "required": [
+                "database",
+                "db",
+                "password",
+                "permission",
+                "username"
+            ],
+            "properties": {
+                "database": {
+                    "type": "string"
+                },
+                "db": {
+                    "type": "string"
+                },
+                "password": {
+                    "type": "string"
+                },
+                "permission": {
+                    "type": "string"
+                },
+                "username": {
+                    "type": "string"
+                }
+            }
+        },
         "dto.CaptchaResponse": {
             "type": "object",
             "properties": {
diff --git a/cmd/server/docs/swagger.json b/cmd/server/docs/swagger.json
index 0e5aebd82..5ef899b02 100644
--- a/cmd/server/docs/swagger.json
+++ b/cmd/server/docs/swagger.json
@@ -3945,6 +3945,49 @@
                 }
             }
         },
+        "/databases/bind": {
+            "post": {
+                "security": [
+                    {
+                        "ApiKeyAuth": []
+                    }
+                ],
+                "description": "绑定 mysql 数据库用户",
+                "consumes": [
+                    "application/json"
+                ],
+                "tags": [
+                    "Database Mysql"
+                ],
+                "summary": "Bind user of mysql database",
+                "parameters": [
+                    {
+                        "description": "request",
+                        "name": "request",
+                        "in": "body",
+                        "required": true,
+                        "schema": {
+                            "$ref": "#/definitions/dto.BindUser"
+                        }
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK"
+                    }
+                },
+                "x-panel-log": {
+                    "BeforeFunctions": [],
+                    "bodyKeys": [
+                        "database",
+                        "username"
+                    ],
+                    "formatEN": "bind mysql database [database] [username]",
+                    "formatZH": "绑定 mysql 数据库名 [database] [username]",
+                    "paramKeys": []
+                }
+            }
+        },
         "/databases/change/access": {
             "post": {
                 "security": [
@@ -10006,7 +10049,7 @@
                     "bodyKeys": [
                         "version"
                     ],
-                    "formatEN": "upgrade service =\u003e [version]",
+                    "formatEN": "upgrade system =\u003e [version]",
                     "formatZH": "更新系统 =\u003e [version]",
                     "paramKeys": []
                 }
@@ -13594,6 +13637,33 @@
                 }
             }
         },
+        "dto.BindUser": {
+            "type": "object",
+            "required": [
+                "database",
+                "db",
+                "password",
+                "permission",
+                "username"
+            ],
+            "properties": {
+                "database": {
+                    "type": "string"
+                },
+                "db": {
+                    "type": "string"
+                },
+                "password": {
+                    "type": "string"
+                },
+                "permission": {
+                    "type": "string"
+                },
+                "username": {
+                    "type": "string"
+                }
+            }
+        },
         "dto.CaptchaResponse": {
             "type": "object",
             "properties": {
diff --git a/cmd/server/docs/swagger.yaml b/cmd/server/docs/swagger.yaml
index 26c908338..f0d01437b 100644
--- a/cmd/server/docs/swagger.yaml
+++ b/cmd/server/docs/swagger.yaml
@@ -137,6 +137,25 @@ definitions:
     - bindAddress
     - ipv6
     type: object
+  dto.BindUser:
+    properties:
+      database:
+        type: string
+      db:
+        type: string
+      password:
+        type: string
+      permission:
+        type: string
+      username:
+        type: string
+    required:
+    - database
+    - db
+    - password
+    - permission
+    - username
+    type: object
   dto.CaptchaResponse:
     properties:
       captchaID:
@@ -7268,6 +7287,34 @@ paths:
       summary: Load mysql base info
       tags:
       - Database Mysql
+  /databases/bind:
+    post:
+      consumes:
+      - application/json
+      description: 绑定 mysql 数据库用户
+      parameters:
+      - description: request
+        in: body
+        name: request
+        required: true
+        schema:
+          $ref: '#/definitions/dto.BindUser'
+      responses:
+        "200":
+          description: OK
+      security:
+      - ApiKeyAuth: []
+      summary: Bind user of mysql database
+      tags:
+      - Database Mysql
+      x-panel-log:
+        BeforeFunctions: []
+        bodyKeys:
+        - database
+        - username
+        formatEN: bind mysql database [database] [username]
+        formatZH: 绑定 mysql 数据库名 [database] [username]
+        paramKeys: []
   /databases/change/access:
     post:
       consumes:
@@ -11106,7 +11153,7 @@ paths:
         BeforeFunctions: []
         bodyKeys:
         - version
-        formatEN: upgrade service => [version]
+        formatEN: upgrade system => [version]
         formatZH: 更新系统 => [version]
         paramKeys: []
   /toolbox/clean:
diff --git a/frontend/src/api/interface/database.ts b/frontend/src/api/interface/database.ts
index 6e3a77e19..c0899ccce 100644
--- a/frontend/src/api/interface/database.ts
+++ b/frontend/src/api/interface/database.ts
@@ -48,6 +48,13 @@ export namespace Database {
         permission: string;
         description: string;
     }
+    export interface BindUser {
+        database: string;
+        db: string;
+        username: string;
+        password: string;
+        permission: string;
+    }
     export interface MysqlLoadDB {
         from: string;
         type: string;
diff --git a/frontend/src/api/modules/database.ts b/frontend/src/api/modules/database.ts
index c92590c16..1186a3042 100644
--- a/frontend/src/api/modules/database.ts
+++ b/frontend/src/api/modules/database.ts
@@ -19,6 +19,13 @@ export const addMysqlDB = (params: Database.MysqlDBCreate) => {
     }
     return http.post(`/databases`, request);
 };
+export const bindUser = (params: Database.BindUser) => {
+    let request = deepCopy(params) as Database.BindUser;
+    if (request.password) {
+        request.password = Base64.encode(request.password);
+    }
+    return http.post(`/databases/bind`, request);
+};
 export const loadDBFromRemote = (params: Database.MysqlLoadDB) => {
     return http.post(`/databases/load`, params);
 };
diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts
index 1a35f171e..b25236353 100644
--- a/frontend/src/lang/modules/en.ts
+++ b/frontend/src/lang/modules/en.ts
@@ -380,6 +380,7 @@ const message = {
             'This port is the exposed port of the container. You need to save the modification separately and restart the container!',
 
         loadFromRemote: 'Load from server',
+        userBind: 'Bind User',
         loadFromRemoteHelper:
             'This action will synchronize the database info on the server to 1Panel, do you want to continue?',
         passwordHelper: 'Unable to retrieve, please modify',
diff --git a/frontend/src/lang/modules/tw.ts b/frontend/src/lang/modules/tw.ts
index 8004bc179..e147f9cb3 100644
--- a/frontend/src/lang/modules/tw.ts
+++ b/frontend/src/lang/modules/tw.ts
@@ -373,6 +373,7 @@ const message = {
         confNotFound: '未能找到該應用配置文件,請在應用商店升級該應用至最新版本後重試!',
 
         loadFromRemote: '從服務器獲取',
+        userBind: '綁定使用者',
         loadFromRemoteHelper: '此操作將同步服務器上數據庫信息到 1Panel,是否繼續?',
         passwordHelper: '無法獲取密碼,請修改',
         local: '本地',
diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts
index 83b710808..2a86c1d04 100644
--- a/frontend/src/lang/modules/zh.ts
+++ b/frontend/src/lang/modules/zh.ts
@@ -373,6 +373,7 @@ const message = {
         confNotFound: '未能找到该应用配置文件,请在应用商店升级该应用至最新版本后重试!',
 
         loadFromRemote: '从服务器获取',
+        userBind: '绑定用户',
         loadFromRemoteHelper: '此操作将同步服务器上数据库信息到 1Panel,是否继续?',
         passwordHelper: '无法获取密码,请修改',
         local: '本地',
diff --git a/frontend/src/views/database/mysql/bind/index.vue b/frontend/src/views/database/mysql/bind/index.vue
new file mode 100644
index 000000000..d3ad0e5fb
--- /dev/null
+++ b/frontend/src/views/database/mysql/bind/index.vue
@@ -0,0 +1,139 @@
+<template>
+    <div>
+        <el-drawer v-model="bindVisible" :destroy-on-close="true" :close-on-click-modal="false" width="30%">
+            <template #header>
+                <DrawerHeader :header="$t('database.userBind')" :resource="form.mysqlName" :back="handleClose" />
+            </template>
+            <el-form v-loading="loading" ref="changeFormRef" :model="form" :rules="rules" label-position="top">
+                <el-row type="flex" justify="center">
+                    <el-col :span="22">
+                        <el-form-item :label="$t('commons.login.username')" prop="username">
+                            <el-input v-model="form.username"></el-input>
+                        </el-form-item>
+                        <el-form-item :label="$t('commons.login.password')" prop="password">
+                            <el-input type="password" clearable show-password v-model="form.password"></el-input>
+                        </el-form-item>
+                        <el-form-item :label="$t('database.permission')" prop="permission">
+                            <el-select v-model="form.permission">
+                                <el-option value="%" :label="$t('database.permissionAll')" />
+                                <el-option
+                                    v-if="form.from !== 'local'"
+                                    value="localhost"
+                                    :label="$t('terminal.localhost')"
+                                />
+                                <el-option value="ip" :label="$t('database.permissionForIP')" />
+                            </el-select>
+                        </el-form-item>
+                        <el-form-item v-if="form.permission === 'ip'" prop="permissionIPs">
+                            <el-input
+                                clearable
+                                :autosize="{ minRows: 2, maxRows: 5 }"
+                                type="textarea"
+                                v-model="form.permissionIPs"
+                            />
+                            <span class="input-help">{{ $t('database.remoteHelper') }}</span>
+                        </el-form-item>
+                    </el-col>
+                </el-row>
+            </el-form>
+            <template #footer>
+                <span class="dialog-footer">
+                    <el-button :disabled="loading" @click="bindVisible = false">
+                        {{ $t('commons.button.cancel') }}
+                    </el-button>
+                    <el-button :disabled="loading" type="primary" @click="onSubmit(changeFormRef)">
+                        {{ $t('commons.button.confirm') }}
+                    </el-button>
+                </span>
+            </template>
+        </el-drawer>
+
+        <ConfirmDialog ref="confirmDialogRef" @confirm="onSubmit"></ConfirmDialog>
+    </div>
+</template>
+<script lang="ts" setup>
+import { reactive, ref } from 'vue';
+import i18n from '@/lang';
+import { ElForm } from 'element-plus';
+import { bindUser } from '@/api/modules/database';
+import DrawerHeader from '@/components/drawer-header/index.vue';
+import { Rules } from '@/global/form-rules';
+import { MsgSuccess } from '@/utils/message';
+import { checkIp } from '@/utils/util';
+
+const loading = ref();
+const bindVisible = ref(false);
+type FormInstance = InstanceType<typeof ElForm>;
+const changeFormRef = ref<FormInstance>();
+const form = reactive({
+    database: '',
+    mysqlName: '',
+    username: '',
+    password: '',
+    permission: '',
+    permissionIPs: '',
+});
+const confirmDialogRef = ref();
+
+const rules = reactive({
+    username: [Rules.requiredInput, Rules.name],
+    password: [Rules.paramComplexity],
+    permission: [Rules.requiredSelect],
+    permissionIPs: [{ validator: checkIPs, trigger: 'blur', required: true }],
+});
+
+function checkIPs(rule: any, value: any, callback: any) {
+    let ips = form.permissionIPs.split(',');
+    for (const item of ips) {
+        if (checkIp(item)) {
+            return callback(new Error(i18n.global.t('commons.rule.ip')));
+        }
+    }
+    callback();
+}
+
+interface DialogProps {
+    database: string;
+    mysqlName: string;
+}
+const acceptParams = (params: DialogProps): void => {
+    form.id = params.id;
+    form.database = params.database;
+    form.mysqlName = params.mysqlName;
+    bindVisible.value = true;
+};
+const emit = defineEmits<{ (e: 'search'): void }>();
+
+const handleClose = () => {
+    bindVisible.value = false;
+};
+
+const onSubmit = async (formEl: FormInstance | undefined) => {
+    if (!formEl) return;
+    formEl.validate(async (valid) => {
+        if (!valid) return;
+        let param = {
+            database: form.database,
+            db: form.mysqlName,
+            username: form.username,
+            password: form.password,
+            permission: form.permission === 'ip' ? form.permissionIPs : form.permission,
+        };
+        loading.value = true;
+        await bindUser(param)
+            .then(() => {
+                loading.value = false;
+                emit('search');
+                bindVisible.value = false;
+                MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
+            })
+            .catch(() => {
+                loading.value = false;
+            });
+    });
+};
+
+defineExpose({
+    acceptParams,
+});
+</script>
diff --git a/frontend/src/views/database/mysql/index.vue b/frontend/src/views/database/mysql/index.vue
index 88bc48b8c..61532f0a5 100644
--- a/frontend/src/views/database/mysql/index.vue
+++ b/frontend/src/views/database/mysql/index.vue
@@ -124,10 +124,24 @@
             <template #main v-if="currentDB">
                 <ComplexTable :pagination-config="paginationConfig" @sort-change="search" @search="search" :data="data">
                     <el-table-column :label="$t('commons.table.name')" prop="name" sortable />
-                    <el-table-column :label="$t('commons.login.username')" prop="username" />
+                    <el-table-column :label="$t('commons.login.username')" prop="username">
+                        <template #default="{ row }">
+                            <div class="flex items-center" v-if="row.username">
+                                <span>
+                                    {{ row.username }}
+                                </span>
+                            </div>
+                            <div v-else>
+                                <el-button style="margin-left: -3px" type="primary" link @click="onBind(row)">
+                                    {{ $t('database.userBind') }}
+                                </el-button>
+                            </div>
+                        </template>
+                    </el-table-column>
                     <el-table-column :label="$t('commons.login.password')" prop="password">
                         <template #default="{ row }">
-                            <div class="flex items-center" v-if="row.password">
+                            <span v-if="row.username === ''">-</span>
+                            <div class="flex items-center" v-if="row.password && row.username">
                                 <div class="star-center" v-if="!row.showPassword">
                                     <span>**********</span>
                                 </div>
@@ -154,10 +168,10 @@
                                     <CopyButton :content="row.password" type="icon" />
                                 </div>
                             </div>
-                            <div v-else>
-                                <el-link @click="onChangePassword(row)">
-                                    <span style="font-size: 12px">{{ $t('database.passwordHelper') }}</span>
-                                </el-link>
+                            <div v-if="row.password === '' && row.username">
+                                <el-button style="margin-left: -3px" link type="primary" @click="onChangePassword(row)">
+                                    {{ $t('database.passwordHelper') }}
+                                </el-button>
                             </div>
                         </template>
                     </el-table-column>
@@ -221,6 +235,7 @@
             </template>
         </el-dialog>
 
+        <BindDialog ref="bindRef" @search="search" />
         <PasswordDialog ref="passwordRef" @search="search" />
         <RootPasswordDialog ref="connRef" />
         <UploadDialog ref="uploadRef" />
@@ -235,6 +250,7 @@
 </template>
 
 <script lang="ts" setup>
+import BindDialog from '@/views/database/mysql/bind/index.vue';
 import OperateDialog from '@/views/database/mysql/create/index.vue';
 import DeleteDialog from '@/views/database/mysql/delete/index.vue';
 import PasswordDialog from '@/views/database/mysql/password/index.vue';
@@ -274,6 +290,7 @@ const dbOptionsRemote = ref<Array<Database.DatabaseOption>>([]);
 const currentDB = ref<Database.DatabaseOption>();
 const currentDBName = ref();
 
+const bindRef = ref();
 const checkRef = ref();
 const deleteRef = ref();
 
@@ -508,6 +525,14 @@ const onDelete = async (row: Database.MysqlDBInfo) => {
     }
 };
 
+const onBind = async (row: Database.MysqlDBInfo) => {
+    let param = {
+        database: currentDBName.value,
+        mysqlName: row.name,
+    };
+    bindRef.value.acceptParams(param);
+};
+
 const onChangePassword = async (row: Database.MysqlDBInfo) => {
     let param = {
         id: row.id,
@@ -525,6 +550,9 @@ const onChangePassword = async (row: Database.MysqlDBInfo) => {
 const buttons = [
     {
         label: i18n.global.t('database.changePassword'),
+        disabled: (row: Database.MysqlDBInfo) => {
+            return !row.username;
+        },
         click: (row: Database.MysqlDBInfo) => {
             onChangePassword(row);
         },