mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2024-11-28 13:31:32 +08:00
feat: Mysql 数据库增删、改密、改权限方法封装
This commit is contained in:
parent
34e8d88a53
commit
bd5dc56b66
13
backend/app/model/database.go
Normal file
13
backend/app/model/database.go
Normal file
@ -0,0 +1,13 @@
|
||||
package model
|
||||
|
||||
type Database struct {
|
||||
BaseModel
|
||||
Name string `json:"name" gorm:"type:varchar(64);not null"`
|
||||
Type string `json:"type" gorm:"type:varchar(64);not null"`
|
||||
Address string `json:"address" gorm:"type:varchar(64);not null"`
|
||||
Port uint `json:"port" gorm:"type:decimal;not null"`
|
||||
Username string `json:"username" gorm:"type:varchar(64)"`
|
||||
Password string `json:"password" gorm:"type:varchar(64)"`
|
||||
Format string `json:"format" gorm:"type:varchar(64)"`
|
||||
Description string `json:"description" gorm:"type:varchar(256);"`
|
||||
}
|
@ -585,7 +585,7 @@ func excSQL(containerName, password, command string) error {
|
||||
cmd := exec.CommandContext(ctx, "docker", "exec", containerName, "mysql", "-uroot", "-p"+password, "-e", command)
|
||||
stdout, err := cmd.CombinedOutput()
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
return buserr.WithDetail(constant.ErrExecTimeOut, containerName, nil)
|
||||
return buserr.New(constant.ErrExecTimeOut)
|
||||
}
|
||||
stdStr := strings.ReplaceAll(string(stdout), "mysql: [Warning] Using a password on the command line interface can be insecure.\n", "")
|
||||
if err != nil || strings.HasPrefix(string(stdStr), "ERROR ") {
|
||||
|
@ -76,7 +76,7 @@ ErrSSLCertificateFormat: 'Certificate file format error, please use pem format'
|
||||
#mysql
|
||||
ErrUserIsExist: "The current user already exists. Please enter a new user"
|
||||
ErrDatabaseIsExist: "The current database already exists. Please enter a new database"
|
||||
ErrExecTimeOut: "SQL execution timed out, please check the {{ .detail }} container"
|
||||
ErrExecTimeOut: "SQL execution timed out, please check the database"
|
||||
|
||||
#redis
|
||||
ErrTypeOfRedis: "The recovery file type does not match the current persistence mode. Modify the file type and try again"
|
||||
|
@ -76,7 +76,7 @@ ErrSSLCertificateFormat: '證書文件格式錯誤,請使用 pem 格式'
|
||||
#mysql
|
||||
ErrUserIsExist: "當前用戶已存在,請重新輸入"
|
||||
ErrDatabaseIsExist: "當前資料庫已存在,請重新輸入"
|
||||
ErrExecTimeOut: "SQL 執行超時,請檢查{{ .detail }}容器"
|
||||
ErrExecTimeOut: "SQL 執行超時,請檢查數據庫"
|
||||
|
||||
#redis
|
||||
ErrTypeOfRedis: "恢復文件類型與當前持久化方式不符,請修改後重試"
|
||||
|
@ -76,7 +76,7 @@ ErrSSLCertificateFormat: '证书文件格式错误,请使用 pem 格式'
|
||||
#mysql
|
||||
ErrUserIsExist: "当前用户已存在,请重新输入"
|
||||
ErrDatabaseIsExist: "当前数据库已存在,请重新输入"
|
||||
ErrExecTimeOut: "SQL 执行超时,请检查{{ .detail }}容器"
|
||||
ErrExecTimeOut: "SQL 执行超时,请检查数据库"
|
||||
|
||||
#redis
|
||||
ErrTypeOfRedis: "恢复文件类型与当前持久化方式不符,请修改后重试"
|
||||
|
@ -35,6 +35,7 @@ func Init() {
|
||||
migrations.AddMfaInterval,
|
||||
migrations.UpdateAppDetail,
|
||||
migrations.EncryptHostPassword,
|
||||
migrations.AddRemoteDB,
|
||||
})
|
||||
if err := m.Migrate(); err != nil {
|
||||
global.LOG.Error(err)
|
||||
|
@ -479,3 +479,13 @@ var EncryptHostPassword = &gormigrate.Migration{
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var AddRemoteDB = &gormigrate.Migration{
|
||||
ID: "20230718-add-remote-db",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
if err := tx.AutoMigrate(&model.Database{}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
41
backend/utils/mysql/client.go
Normal file
41
backend/utils/mysql/client.go
Normal file
@ -0,0 +1,41 @@
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/backend/buserr"
|
||||
"github.com/1Panel-dev/1Panel/backend/constant"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/mysql/client"
|
||||
)
|
||||
|
||||
type MysqlClient interface {
|
||||
Create(info client.CreateInfo) error
|
||||
Delete(info client.DeleteInfo) error
|
||||
|
||||
ChangePassword(info client.PasswordChangeInfo) error
|
||||
ChangeAccess(info client.AccessChangeInfo) error
|
||||
|
||||
Close()
|
||||
}
|
||||
|
||||
func NewMysqlClient(conn client.DBInfo) (MysqlClient, error) {
|
||||
if conn.Type == "remote" {
|
||||
connArgs := fmt.Sprintf("%s:%s@tcp(%s:%d)/?charset=utf8", conn.UserName, conn.Password, conn.Address, conn.Port)
|
||||
db, err := sql.Open("mysql", connArgs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client.NewRemote(db), nil
|
||||
}
|
||||
if conn.Type == "local" {
|
||||
if cmd.CheckIllegal(conn.Address, conn.UserName, conn.Password) {
|
||||
return nil, buserr.New(constant.ErrCmdIllegal)
|
||||
}
|
||||
connArgs := []string{"exec", conn.Address, "mysql", "-u" + conn.UserName, "-p" + conn.Password + "-e"}
|
||||
return client.NewLocal(connArgs, conn.Address), nil
|
||||
}
|
||||
return nil, errors.New("no such type")
|
||||
}
|
60
backend/utils/mysql/client/info.go
Normal file
60
backend/utils/mysql/client/info.go
Normal file
@ -0,0 +1,60 @@
|
||||
package client
|
||||
|
||||
type DBInfo struct {
|
||||
Type string `json:"type"` // local remote
|
||||
Address string `json:"address"`
|
||||
Port uint `json:"port"`
|
||||
UserName string `json:"userName"`
|
||||
Password string `json:"password"`
|
||||
Format string `json:"format"`
|
||||
|
||||
Timeout uint `json:"timeout"` // second
|
||||
}
|
||||
|
||||
type CreateInfo struct {
|
||||
Name string `json:"name"`
|
||||
Format string `json:"format"`
|
||||
Version string `json:"version"`
|
||||
UserName string `json:"userName"`
|
||||
Password string `json:"password"`
|
||||
Permission string `json:"permission"`
|
||||
|
||||
Timeout uint `json:"timeout"` // second
|
||||
}
|
||||
|
||||
type DeleteInfo struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
UserName string `json:"userName"`
|
||||
Permission string `json:"permission"`
|
||||
|
||||
ForceDelete bool `json:"forceDelete"`
|
||||
Timeout uint `json:"timeout"` // second
|
||||
}
|
||||
|
||||
type PasswordChangeInfo struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
UserName string `json:"userName"`
|
||||
Password string `json:"password"`
|
||||
Permission string `json:"permission"`
|
||||
|
||||
Timeout uint `json:"timeout"` // second
|
||||
}
|
||||
|
||||
type AccessChangeInfo struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
UserName string `json:"userName"`
|
||||
OldPermission string `json:"oldPermission"`
|
||||
Permission string `json:"permission"`
|
||||
|
||||
Timeout uint `json:"timeout"` // second
|
||||
}
|
||||
|
||||
var formatMap = map[string]string{
|
||||
"utf8": "utf8_general_ci",
|
||||
"utf8mb4": "utf8mb4_general_ci",
|
||||
"gbk": "gbk_chinese_ci",
|
||||
"big5": "big5_chinese_ci",
|
||||
}
|
228
backend/utils/mysql/client/local.go
Normal file
228
backend/utils/mysql/client/local.go
Normal file
@ -0,0 +1,228 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/backend/buserr"
|
||||
"github.com/1Panel-dev/1Panel/backend/constant"
|
||||
"github.com/1Panel-dev/1Panel/backend/global"
|
||||
)
|
||||
|
||||
type Local struct {
|
||||
PrefixCommand []string
|
||||
ContainerName string
|
||||
}
|
||||
|
||||
func NewLocal(command []string, containerName string) *Local {
|
||||
return &Local{PrefixCommand: command, ContainerName: containerName}
|
||||
}
|
||||
|
||||
func (r *Local) Create(info CreateInfo) error {
|
||||
createSql := fmt.Sprintf("create database `%s` default character set %s collate %s", info.Name, info.Format, formatMap[info.Format])
|
||||
if err := r.ExecSQL(createSql, info.Timeout); err != nil {
|
||||
if strings.Contains(err.Error(), "ERROR 1007") {
|
||||
return buserr.New(constant.ErrDatabaseIsExist)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if err := r.CreateUser(CreateInfo{Name: info.Name, Version: info.Version, UserName: info.UserName, Permission: info.Permission, Timeout: info.Timeout}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Local) CreateUser(info CreateInfo) error {
|
||||
var userlist []string
|
||||
if strings.Contains(info.Permission, ",") {
|
||||
ips := strings.Split(info.Permission, ",")
|
||||
for _, ip := range ips {
|
||||
if len(ip) != 0 {
|
||||
userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.UserName, ip))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.UserName, info.Permission))
|
||||
}
|
||||
|
||||
for _, user := range userlist {
|
||||
if err := r.ExecSQL(fmt.Sprintf("create user %s identified by '%s';", user, info.Password), info.Timeout); err != nil {
|
||||
_ = r.Delete(DeleteInfo{
|
||||
Name: info.Name,
|
||||
Version: info.Version,
|
||||
UserName: info.UserName,
|
||||
Permission: info.Permission,
|
||||
ForceDelete: true,
|
||||
Timeout: 300})
|
||||
if strings.Contains(err.Error(), "ERROR 1396") {
|
||||
return buserr.New(constant.ErrUserIsExist)
|
||||
}
|
||||
return err
|
||||
}
|
||||
grantStr := fmt.Sprintf("grant all privileges on `%s`.* to %s", info.Name, user)
|
||||
if info.Name == "*" {
|
||||
grantStr = fmt.Sprintf("grant all privileges on *.* to %s", user)
|
||||
}
|
||||
if strings.HasPrefix(info.Version, "5.7") || strings.HasPrefix(info.Version, "5.6") {
|
||||
grantStr = fmt.Sprintf("%s identified by '%s' with grant option;", grantStr, info.Password)
|
||||
}
|
||||
if err := r.ExecSQL(grantStr, info.Timeout); err != nil {
|
||||
_ = r.Delete(DeleteInfo{
|
||||
Name: info.Name,
|
||||
Version: info.Version,
|
||||
UserName: info.UserName,
|
||||
Permission: info.Permission,
|
||||
ForceDelete: true,
|
||||
Timeout: 300})
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Local) Delete(info DeleteInfo) error {
|
||||
var userlist []string
|
||||
if strings.Contains(info.Permission, ",") {
|
||||
ips := strings.Split(info.Permission, ",")
|
||||
for _, ip := range ips {
|
||||
if len(ip) != 0 {
|
||||
userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.UserName, ip))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.UserName, info.Permission))
|
||||
}
|
||||
|
||||
for _, user := range userlist {
|
||||
if strings.HasPrefix(info.Version, "5.6") {
|
||||
if err := r.ExecSQL(fmt.Sprintf("drop user %s", user), info.Timeout); err != nil && !info.ForceDelete {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := r.ExecSQL(fmt.Sprintf("drop user if exists %s", user), info.Timeout); err != nil && !info.ForceDelete {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(info.Name) != 0 {
|
||||
if err := r.ExecSQL(fmt.Sprintf("drop database if exists `%s`", info.Name), info.Timeout); err != nil && !info.ForceDelete {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !info.ForceDelete {
|
||||
global.LOG.Info("execute delete database sql successful, now start to drop uploads and records")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Local) ChangePassword(info PasswordChangeInfo) error {
|
||||
if info.UserName != "root" {
|
||||
var userlist []string
|
||||
if strings.Contains(info.Permission, ",") {
|
||||
ips := strings.Split(info.Permission, ",")
|
||||
for _, ip := range ips {
|
||||
if len(ip) != 0 {
|
||||
userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.UserName, ip))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.UserName, info.Permission))
|
||||
}
|
||||
|
||||
for _, user := range userlist {
|
||||
passwordChangeSql := fmt.Sprintf("set password for %s = password('%s')", user, info.Password)
|
||||
if !strings.HasPrefix(info.Version, "5.7") && !strings.HasPrefix(info.Version, "5.6") {
|
||||
passwordChangeSql = fmt.Sprintf("ALTER USER %s IDENTIFIED WITH mysql_native_password BY '%s';", user, info.Password)
|
||||
}
|
||||
if err := r.ExecSQL(passwordChangeSql, info.Timeout); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
hosts, err := r.ExecSQLForRows("select host from mysql.user where user='root';", info.Timeout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, host := range hosts {
|
||||
if host == "%" || host == "localhost" {
|
||||
passwordRootChangeCMD := fmt.Sprintf("set password for 'root'@'%s' = password('%s')", host, info.Password)
|
||||
if !strings.HasPrefix(info.Version, "5.7") && !strings.HasPrefix(info.Version, "5.6") {
|
||||
passwordRootChangeCMD = fmt.Sprintf("alter user 'root'@'%s' identified with mysql_native_password BY '%s';", host, info.Password)
|
||||
}
|
||||
if err := r.ExecSQL(passwordRootChangeCMD, info.Timeout); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Local) ChangeAccess(info AccessChangeInfo) error {
|
||||
if info.UserName == "root" {
|
||||
info.OldPermission = "%"
|
||||
info.Name = "*"
|
||||
}
|
||||
if info.Permission != info.OldPermission {
|
||||
if err := r.Delete(DeleteInfo{
|
||||
Version: info.Version,
|
||||
UserName: info.UserName,
|
||||
Permission: info.OldPermission,
|
||||
ForceDelete: true,
|
||||
Timeout: 300}); err != nil {
|
||||
return err
|
||||
}
|
||||
if info.UserName == "root" {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if err := r.CreateUser(CreateInfo{Name: info.Name, Version: info.Version, UserName: info.UserName, Permission: info.Permission, Timeout: info.Timeout}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Local) Close() {}
|
||||
|
||||
func (r *Local) ExecSQL(command string, timeout uint) error {
|
||||
itemCommand := r.PrefixCommand[:]
|
||||
itemCommand = append(itemCommand, command)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
|
||||
defer cancel()
|
||||
cmd := exec.CommandContext(ctx, "docker", itemCommand...)
|
||||
stdout, err := cmd.CombinedOutput()
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
return buserr.New(constant.ErrExecTimeOut)
|
||||
}
|
||||
stdStr := strings.ReplaceAll(string(stdout), "mysql: [Warning] Using a password on the command line interface can be insecure.\n", "")
|
||||
if err != nil || strings.HasPrefix(string(stdStr), "ERROR ") {
|
||||
return errors.New(stdStr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Local) ExecSQLForRows(command string, timeout uint) ([]string, error) {
|
||||
itemCommand := r.PrefixCommand[:]
|
||||
itemCommand = append(itemCommand, command)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
|
||||
defer cancel()
|
||||
cmd := exec.CommandContext(ctx, "docker", itemCommand...)
|
||||
stdout, err := cmd.CombinedOutput()
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
return nil, buserr.New(constant.ErrExecTimeOut)
|
||||
}
|
||||
stdStr := strings.ReplaceAll(string(stdout), "mysql: [Warning] Using a password on the command line interface can be insecure.\n", "")
|
||||
if err != nil || strings.HasPrefix(string(stdStr), "ERROR ") {
|
||||
return nil, errors.New(stdStr)
|
||||
}
|
||||
return strings.Split(stdStr, "\n"), nil
|
||||
}
|
230
backend/utils/mysql/client/remote.go
Normal file
230
backend/utils/mysql/client/remote.go
Normal file
@ -0,0 +1,230 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/backend/buserr"
|
||||
"github.com/1Panel-dev/1Panel/backend/constant"
|
||||
"github.com/1Panel-dev/1Panel/backend/global"
|
||||
)
|
||||
|
||||
type Remote struct {
|
||||
Client *sql.DB
|
||||
}
|
||||
|
||||
func NewRemote(client *sql.DB) *Remote {
|
||||
return &Remote{Client: client}
|
||||
}
|
||||
|
||||
func (r *Remote) Create(info CreateInfo) error {
|
||||
createSql := fmt.Sprintf("create database `%s` default character set %s collate %s", info.Name, info.Format, formatMap[info.Format])
|
||||
if err := r.ExecSQL(createSql, info.Timeout); err != nil {
|
||||
if strings.Contains(err.Error(), "ERROR 1007") {
|
||||
return buserr.New(constant.ErrDatabaseIsExist)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if err := r.CreateUser(CreateInfo{Name: info.Name, Version: info.Version, UserName: info.UserName, Permission: info.Permission, Timeout: info.Timeout}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Remote) CreateUser(info CreateInfo) error {
|
||||
var userlist []string
|
||||
if strings.Contains(info.Permission, ",") {
|
||||
ips := strings.Split(info.Permission, ",")
|
||||
for _, ip := range ips {
|
||||
if len(ip) != 0 {
|
||||
userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.UserName, ip))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.UserName, info.Permission))
|
||||
}
|
||||
|
||||
for _, user := range userlist {
|
||||
if err := r.ExecSQL(fmt.Sprintf("create user %s identified by '%s';", user, info.Password), info.Timeout); err != nil {
|
||||
_ = r.Delete(DeleteInfo{
|
||||
Name: info.Name,
|
||||
Version: info.Version,
|
||||
UserName: info.UserName,
|
||||
Permission: info.Permission,
|
||||
ForceDelete: true,
|
||||
Timeout: 300})
|
||||
if strings.Contains(err.Error(), "ERROR 1396") {
|
||||
return buserr.New(constant.ErrUserIsExist)
|
||||
}
|
||||
return err
|
||||
}
|
||||
grantStr := fmt.Sprintf("grant all privileges on `%s`.* to %s", info.Name, user)
|
||||
if info.Name == "*" {
|
||||
grantStr = fmt.Sprintf("grant all privileges on *.* to %s", user)
|
||||
}
|
||||
if strings.HasPrefix(info.Version, "5.7") || strings.HasPrefix(info.Version, "5.6") {
|
||||
grantStr = fmt.Sprintf("%s identified by '%s' with grant option;", grantStr, info.Password)
|
||||
}
|
||||
if err := r.ExecSQL(grantStr, info.Timeout); err != nil {
|
||||
_ = r.Delete(DeleteInfo{
|
||||
Name: info.Name,
|
||||
Version: info.Version,
|
||||
UserName: info.UserName,
|
||||
Permission: info.Permission,
|
||||
ForceDelete: true,
|
||||
Timeout: 300})
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Remote) Delete(info DeleteInfo) error {
|
||||
var userlist []string
|
||||
if strings.Contains(info.Permission, ",") {
|
||||
ips := strings.Split(info.Permission, ",")
|
||||
for _, ip := range ips {
|
||||
if len(ip) != 0 {
|
||||
userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.UserName, ip))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.UserName, info.Permission))
|
||||
}
|
||||
|
||||
for _, user := range userlist {
|
||||
if strings.HasPrefix(info.Version, "5.6") {
|
||||
if err := r.ExecSQL(fmt.Sprintf("drop user %s", user), info.Timeout); err != nil && !info.ForceDelete {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := r.ExecSQL(fmt.Sprintf("drop user if exists %s", user), info.Timeout); err != nil && !info.ForceDelete {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(info.Name) != 0 {
|
||||
if err := r.ExecSQL(fmt.Sprintf("drop database if exists `%s`", info.Name), info.Timeout); err != nil && !info.ForceDelete {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !info.ForceDelete {
|
||||
global.LOG.Info("execute delete database sql successful, now start to drop uploads and records")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Remote) ChangePassword(info PasswordChangeInfo) error {
|
||||
if info.UserName != "root" {
|
||||
var userlist []string
|
||||
if strings.Contains(info.Permission, ",") {
|
||||
ips := strings.Split(info.Permission, ",")
|
||||
for _, ip := range ips {
|
||||
if len(ip) != 0 {
|
||||
userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.UserName, ip))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.UserName, info.Permission))
|
||||
}
|
||||
|
||||
for _, user := range userlist {
|
||||
passwordChangeSql := fmt.Sprintf("set password for %s = password('%s')", user, info.Password)
|
||||
if !strings.HasPrefix(info.Version, "5.7") && !strings.HasPrefix(info.Version, "5.6") {
|
||||
passwordChangeSql = fmt.Sprintf("ALTER USER %s IDENTIFIED WITH mysql_native_password BY '%s';", user, info.Password)
|
||||
}
|
||||
if err := r.ExecSQL(passwordChangeSql, info.Timeout); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
hosts, err := r.ExecSQLForHosts(info.Timeout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, host := range hosts {
|
||||
if host == "%" || host == "localhost" {
|
||||
passwordRootChangeCMD := fmt.Sprintf("set password for 'root'@'%s' = password('%s')", host, info.Password)
|
||||
if !strings.HasPrefix(info.Version, "5.7") && !strings.HasPrefix(info.Version, "5.6") {
|
||||
passwordRootChangeCMD = fmt.Sprintf("alter user 'root'@'%s' identified with mysql_native_password BY '%s';", host, info.Password)
|
||||
}
|
||||
if err := r.ExecSQL(passwordRootChangeCMD, info.Timeout); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Remote) ChangeAccess(info AccessChangeInfo) error {
|
||||
if info.UserName == "root" {
|
||||
info.OldPermission = "%"
|
||||
info.Name = "*"
|
||||
}
|
||||
if info.Permission != info.OldPermission {
|
||||
if err := r.Delete(DeleteInfo{
|
||||
Version: info.Version,
|
||||
UserName: info.UserName,
|
||||
Permission: info.OldPermission,
|
||||
ForceDelete: true,
|
||||
Timeout: 300}); err != nil {
|
||||
return err
|
||||
}
|
||||
if info.UserName == "root" {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if err := r.CreateUser(CreateInfo{Name: info.Name, Version: info.Version, UserName: info.UserName, Permission: info.Permission, Timeout: info.Timeout}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Remote) Close() {
|
||||
_ = r.Client.Close()
|
||||
}
|
||||
|
||||
func (r *Remote) ExecSQL(command string, timeout uint) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if _, err := r.Client.ExecContext(ctx, command); err != nil {
|
||||
return err
|
||||
}
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
return buserr.New(constant.ErrExecTimeOut)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Remote) ExecSQLForHosts(timeout uint) ([]string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
results, err := r.Client.QueryContext(ctx, "select host from mysql.user where user='root';")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
return nil, buserr.New(constant.ErrExecTimeOut)
|
||||
}
|
||||
var rows []string
|
||||
for results.Next() {
|
||||
var host string
|
||||
if err := results.Scan(&host); err != nil {
|
||||
continue
|
||||
}
|
||||
rows = append(rows, host)
|
||||
}
|
||||
return rows, nil
|
||||
}
|
@ -160,4 +160,17 @@ export namespace Database {
|
||||
fileName: string;
|
||||
fileDir: string;
|
||||
}
|
||||
|
||||
// remote
|
||||
export interface RemoteDBInfo {
|
||||
id: number;
|
||||
createdAt: Date;
|
||||
name: string;
|
||||
type: string;
|
||||
address: string;
|
||||
port: number;
|
||||
username: string;
|
||||
password: string;
|
||||
description: string;
|
||||
}
|
||||
}
|
||||
|
@ -83,3 +83,17 @@ export const updateRedisConf = (params: Database.RedisConfUpdate) => {
|
||||
export const updateRedisConfByFile = (params: Database.RedisConfUpdateByFile) => {
|
||||
return http.post(`/databases/redis/conffile/update`, params);
|
||||
};
|
||||
|
||||
// remote
|
||||
export const searchRemoteDBs = (params: SearchWithPage) => {
|
||||
return http.post<ResPage<Database.RemoteDBInfo>>(`/databases/remote/search`, params);
|
||||
};
|
||||
export const addRemoteDB = (params: Database.RemoteDBInfo) => {
|
||||
return http.post(`/databases/remote`, params);
|
||||
};
|
||||
export const editRemoteDB = (params: Database.RemoteDBInfo) => {
|
||||
return http.post(`/databases/remote/update`, params);
|
||||
};
|
||||
export const deleteRemoteDB = (id: number) => {
|
||||
return http.post(`/databases/remote/del`, { id: id });
|
||||
};
|
||||
|
152
frontend/src/views/database/remote-db/index.vue
Normal file
152
frontend/src/views/database/remote-db/index.vue
Normal file
@ -0,0 +1,152 @@
|
||||
<template>
|
||||
<div v-loading="loading">
|
||||
<LayoutContent :title="'MySQL ' + $t('menu.database')">
|
||||
<template #toolbar>
|
||||
<el-row>
|
||||
<el-col :xs="24" :sm="20" :md="20" :lg="20" :xl="20">
|
||||
<el-button type="primary" @click="onOpenDialog('create')">
|
||||
{{ $t('database.create') }}
|
||||
</el-button>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="4" :md="4" :lg="4" :xl="4">
|
||||
<div class="search-button">
|
||||
<el-input
|
||||
v-model="searchName"
|
||||
clearable
|
||||
@clear="search()"
|
||||
suffix-icon="Search"
|
||||
@keyup.enter="search()"
|
||||
@change="search()"
|
||||
:placeholder="$t('commons.button.search')"
|
||||
></el-input>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
<template #main>
|
||||
<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="address" />
|
||||
<el-table-column :label="$t('commons.login.username')" prop="username" />
|
||||
<el-table-column :label="$t('commons.login.password')" prop="password">
|
||||
<template #default="{ row }">
|
||||
<div>
|
||||
<span style="float: left; line-height: 25px" v-if="!row.showPassword">***********</span>
|
||||
<div style="cursor: pointer; float: left" v-if="!row.showPassword">
|
||||
<el-icon
|
||||
style="margin-left: 5px; margin-top: 3px"
|
||||
@click="row.showPassword = true"
|
||||
:size="16"
|
||||
>
|
||||
<View />
|
||||
</el-icon>
|
||||
</div>
|
||||
<span style="float: left" v-if="row.showPassword">{{ row.password }}</span>
|
||||
<div style="cursor: pointer; float: left" v-if="row.showPassword">
|
||||
<el-icon
|
||||
style="margin-left: 5px; margin-top: 3px"
|
||||
@click="row.showPassword = false"
|
||||
:size="16"
|
||||
>
|
||||
<Hide />
|
||||
</el-icon>
|
||||
</div>
|
||||
<div style="cursor: pointer; float: left">
|
||||
<el-icon style="margin-left: 5px; margin-top: 3px" :size="16" @click="onCopy(row)">
|
||||
<DocumentCopy />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="createdAt"
|
||||
:label="$t('commons.table.date')"
|
||||
:formatter="dateFormat"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
</ComplexTable>
|
||||
</template>
|
||||
</LayoutContent>
|
||||
|
||||
<OperateDialog ref="dialogRef" @search="search" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { dateFormat } from '@/utils/util';
|
||||
import { reactive, ref } from 'vue';
|
||||
import { searchRemoteDBs } from '@/api/modules/database';
|
||||
import i18n from '@/lang';
|
||||
import { MsgError, MsgSuccess } from '@/utils/message';
|
||||
import useClipboard from 'vue-clipboard3';
|
||||
import { Database } from '@/api/interface/database';
|
||||
const { toClipboard } = useClipboard();
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
const dialogRef = ref();
|
||||
|
||||
const data = ref();
|
||||
const paginationConfig = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
});
|
||||
const searchName = ref();
|
||||
|
||||
const search = async (column?: any) => {
|
||||
let params = {
|
||||
page: paginationConfig.currentPage,
|
||||
pageSize: paginationConfig.pageSize,
|
||||
info: searchName.value,
|
||||
orderBy: column?.order ? column.prop : 'created_at',
|
||||
order: column?.order ? column.order : 'null',
|
||||
};
|
||||
const res = await searchRemoteDBs(params);
|
||||
data.value = res.data.items || [];
|
||||
paginationConfig.total = res.data.total;
|
||||
};
|
||||
|
||||
const onOpenDialog = async (
|
||||
title: string,
|
||||
rowData: Partial<Database.RemoteDBInfo> = {
|
||||
name: '',
|
||||
type: 'Mysql',
|
||||
address: '',
|
||||
port: 3306,
|
||||
username: '',
|
||||
password: '',
|
||||
description: '',
|
||||
},
|
||||
) => {
|
||||
let params = {
|
||||
title,
|
||||
rowData: { ...rowData },
|
||||
};
|
||||
dialogRef.value!.acceptParams(params);
|
||||
};
|
||||
|
||||
const onCopy = async (row: any) => {
|
||||
try {
|
||||
await toClipboard(row.password);
|
||||
MsgSuccess(i18n.global.t('commons.msg.copySuccess'));
|
||||
} catch (e) {
|
||||
MsgError(i18n.global.t('commons.msg.copyfailed'));
|
||||
}
|
||||
};
|
||||
|
||||
// const onDelete = async (row: Database.MysqlDBInfo) => {
|
||||
// const res = await deleteCheckMysqlDB(row.id);
|
||||
// deleteRef.value.acceptParams({ id: row.id, name: row.name });
|
||||
// };
|
||||
|
||||
// const buttons = [
|
||||
// {
|
||||
// label: i18n.global.t('commons.button.delete'),
|
||||
// click: (row: Database.MysqlDBInfo) => {
|
||||
// onDelete(row);
|
||||
// },
|
||||
// },
|
||||
// ];
|
||||
</script>
|
112
frontend/src/views/database/remote-db/operate/index.vue
Normal file
112
frontend/src/views/database/remote-db/operate/index.vue
Normal file
@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<el-drawer v-model="drawerVisiable" :destroy-on-close="true" :close-on-click-modal="false" size="50%">
|
||||
<template #header>
|
||||
<DrawerHeader :header="title" :resource="dialogData.rowData?.name" :back="handleClose" />
|
||||
</template>
|
||||
<el-form ref="formRef" label-position="top" :model="dialogData.rowData" :rules="rules">
|
||||
<el-row type="flex" justify="center">
|
||||
<el-col :span="22">
|
||||
<el-form-item :label="$t('cronjob.taskName')" prop="name">
|
||||
<el-input
|
||||
:disabled="dialogData.title === 'edit'"
|
||||
clearable
|
||||
v-model.trim="dialogData.rowData!.name"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('cronjob.taskType')" prop="type">
|
||||
<el-select v-model="dialogData.rowData!.type">
|
||||
<el-option value="Mysql" label="Mysql" />
|
||||
<el-option value="Redis" label="Redis" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="$t('cronjob.taskName')" prop="address">
|
||||
<el-input clearable v-model.trim="dialogData.rowData!.address" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('cronjob.taskName')" prop="port">
|
||||
<el-input clearable v-model.number="dialogData.rowData!.port" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('cronjob.taskName')" prop="username">
|
||||
<el-input clearable v-model.trim="dialogData.rowData!.username" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('cronjob.taskName')" prop="password">
|
||||
<el-input clearable v-model.trim="dialogData.rowData!.password" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('cronjob.description')" prop="description">
|
||||
<el-input clearable v-model.trim="dialogData.rowData!.description" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="drawerVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="onSubmit(formRef)">
|
||||
{{ $t('commons.button.confirm') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from 'vue';
|
||||
import i18n from '@/lang';
|
||||
import { ElForm } from 'element-plus';
|
||||
import { Cronjob } from '@/api/interface/cronjob';
|
||||
import { addCronjob, editCronjob } from '@/api/modules/cronjob';
|
||||
import DrawerHeader from '@/components/drawer-header/index.vue';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
|
||||
interface DialogProps {
|
||||
title: string;
|
||||
rowData?: Cronjob.CronjobInfo;
|
||||
getTableList?: () => Promise<any>;
|
||||
}
|
||||
const title = ref<string>('');
|
||||
const drawerVisiable = ref(false);
|
||||
const dialogData = ref<DialogProps>({
|
||||
title: '',
|
||||
});
|
||||
const acceptParams = (params: DialogProps): void => {
|
||||
dialogData.value = params;
|
||||
title.value = i18n.global.t('cronjob.' + dialogData.value.title);
|
||||
drawerVisiable.value = true;
|
||||
};
|
||||
const emit = defineEmits<{ (e: 'search'): void }>();
|
||||
|
||||
const handleClose = () => {
|
||||
drawerVisiable.value = false;
|
||||
};
|
||||
|
||||
const rules = reactive({
|
||||
name: [Rules.requiredInput],
|
||||
type: [Rules.requiredSelect],
|
||||
address: [Rules.requiredInput],
|
||||
port: [Rules.port],
|
||||
});
|
||||
|
||||
type FormInstance = InstanceType<typeof ElForm>;
|
||||
const formRef = ref<FormInstance>();
|
||||
|
||||
const onSubmit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return;
|
||||
formEl.validate(async (valid) => {
|
||||
if (!valid) return;
|
||||
if (dialogData.value.title === 'create') {
|
||||
await addCronjob(dialogData.value.rowData);
|
||||
}
|
||||
if (dialogData.value.title === 'edit') {
|
||||
await editCronjob(dialogData.value.rowData);
|
||||
}
|
||||
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
emit('search');
|
||||
drawerVisiable.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
</script>
|
Loading…
Reference in New Issue
Block a user