1
0
mirror of https://github.com/1Panel-dev/1Panel.git synced 2025-01-31 14:08:06 +08:00

feat: 实现远程数据库增删改查

This commit is contained in:
ssongliu 2023-07-20 17:51:57 +08:00 committed by ssongliu
parent bd5dc56b66
commit cb7351a9fb
26 changed files with 931 additions and 552 deletions

View File

@ -21,8 +21,9 @@ var (
imageService = service.NewIImageService()
dockerService = service.NewIDockerService()
mysqlService = service.NewIMysqlService()
redisService = service.NewIRedisService()
mysqlService = service.NewIMysqlService()
remoteDBService = service.NewIRemoteDBService()
redisService = service.NewIRedisService()
cronjobService = service.NewICronjobService()

View File

@ -0,0 +1,133 @@
package v1
import (
"github.com/1Panel-dev/1Panel/backend/app/api/v1/helper"
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/gin-gonic/gin"
)
// @Tags Database
// @Summary Create remote database
// @Description 创建远程数据库
// @Accept json
// @Param request body dto.DatabaseCreate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /databases/remote [post]
// @x-panel-log {"bodyKeys":["name", "type"],"paramKeys":[],"BeforeFuntions":[],"formatZH":"创建远程数据库 [name][type]","formatEN":"create remote database [name][type]"}
func (b *BaseApi) CreateRemoteDB(c *gin.Context) {
var req dto.RemoteDBCreate
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := global.VALID.Struct(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := remoteDBService.Create(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}
// @Tags Database
// @Summary Page remote databases
// @Description 获取远程数据库列表分页
// @Accept json
// @Param request body dto.SearchWithPage true "request"
// @Success 200 {object} dto.PageResult
// @Security ApiKeyAuth
// @Router /databases/remote/search [post]
func (b *BaseApi) SearchRemoteDB(c *gin.Context) {
var req dto.RemoteDBSearch
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
total, list, err := remoteDBService.SearchWithPage(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, dto.PageResult{
Items: list,
Total: total,
})
}
// @Tags Database
// @Summary List remote databases
// @Description 获取快速命令列表
// @Success 200 {array} dto.RemoteDBOption
// @Security ApiKeyAuth
// @Router /databases/remote/list/:type [get]
func (b *BaseApi) ListRemoteDB(c *gin.Context) {
dbType := c.Query("type")
list, err := remoteDBService.List(dbType)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, list)
}
// @Tags Database
// @Summary Delete remote database
// @Description 删除远程数据库
// @Accept json
// @Param request body dto.OperateByID true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /databases/remote/del [post]
// @x-panel-log {"bodyKeys":["ids"],"paramKeys":[],"BeforeFuntions":[{"input_column":"id","input_value":"ids","isList":true,"db":"databases","output_column":"name","output_value":"names"}],"formatZH":"删除远程数据库 [names]","formatEN":"delete remote database [names]"}
func (b *BaseApi) DeleteRemoteDB(c *gin.Context) {
var req dto.OperateByID
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := global.VALID.Struct(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := remoteDBService.Delete(req.ID); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}
// @Tags Database
// @Summary Update remote database
// @Description 更新远程数据库
// @Accept json
// @Param request body dto.DatabaseUpdate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /databases/remote/update [post]
// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFuntions":[],"formatZH":"更新远程数据库 [name]","formatEN":"update remote database [name]"}
func (b *BaseApi) UpdateRemoteDB(c *gin.Context) {
var req dto.RemoteDBUpdate
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := global.VALID.Struct(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := remoteDBService.Update(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}

View File

@ -16,6 +16,7 @@ type MysqlDBInfo struct {
type MysqlDBCreate struct {
Name string `json:"name" validate:"required"`
From string `json:"from" validate:"required"`
Format string `json:"format" validate:"required,oneof=utf8mb4 utf8 gbk big5"`
Username string `json:"username" validate:"required"`
Password string `json:"password" validate:"required"`
@ -100,6 +101,7 @@ type MysqlConfUpdateByFile struct {
type ChangeDBInfo struct {
ID uint `json:"id"`
From string `json:"from" validate:"required"`
Value string `json:"value" validate:"required"`
}

View File

@ -0,0 +1,52 @@
package dto
import "time"
type RemoteDBSearch struct {
PageInfo
Info string `json:"info"`
Type string `json:"type"`
OrderBy string `json:"orderBy"`
Order string `json:"order"`
}
type RemoteDBInfo struct {
ID uint `json:"id"`
CreatedAt time.Time `json:"createdAt"`
Name string `json:"name"`
From string `json:"from"`
Version string `json:"version"`
Address string `json:"address"`
Port uint `json:"port"`
Username string `json:"username"`
Password string `json:"password"`
Description string `json:"description"`
}
type RemoteDBOption struct {
ID uint `json:"id"`
Name string `json:"name"`
Address string `json:"address"`
}
type RemoteDBCreate struct {
Name string `json:"name" validate:"required"`
Type string `json:"type" validate:"required,oneof=mysql"`
From string `json:"from" validate:"required,oneof=local remote"`
Version string `json:"version" validate:"required"`
Address string `json:"address"`
Port uint `json:"port"`
Username string `json:"username" validate:"required"`
Password string `json:"password" validate:"required"`
Description string `json:"description"`
}
type RemoteDBUpdate struct {
ID uint `json:"id"`
Version string `json:"version" validate:"required"`
Address string `json:"address"`
Port uint `json:"port"`
Username string `json:"username" validate:"required"`
Password string `json:"password" validate:"required"`
Description string `json:"description"`
}

View File

@ -3,6 +3,7 @@ package model
type DatabaseMysql struct {
BaseModel
Name string `json:"name" gorm:"type:varchar(256);not null"`
From string `json:"type" gorm:"type:varchar(256);not null"`
MysqlName string `json:"mysqlName" gorm:"type:varchar(64);not null"`
Format string `json:"format" gorm:"type:varchar(64);not null"`
Username string `json:"username" gorm:"type:varchar(256);not null"`

View File

@ -1,13 +1,14 @@
package model
type Database struct {
type RemoteDB struct {
BaseModel
Name string `json:"name" gorm:"type:varchar(64);not null"`
Type string `json:"type" gorm:"type:varchar(64);not null"`
Version string `json:"version" gorm:"type:varchar(64);not null"`
From string `json:"from" 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);"`
}

View File

@ -0,0 +1,77 @@
package repo
import (
"github.com/1Panel-dev/1Panel/backend/app/model"
"github.com/1Panel-dev/1Panel/backend/global"
"gorm.io/gorm"
)
type RemoteDBRepo struct{}
type IRemoteDBRepo interface {
GetList(opts ...DBOption) ([]model.RemoteDB, error)
Page(limit, offset int, opts ...DBOption) (int64, []model.RemoteDB, error)
Create(database *model.RemoteDB) error
Update(id uint, vars map[string]interface{}) error
Delete(opts ...DBOption) error
Get(opts ...DBOption) (model.RemoteDB, error)
WithoutByFrom(from string) DBOption
}
func NewIRemoteDBRepo() IRemoteDBRepo {
return &RemoteDBRepo{}
}
func (u *RemoteDBRepo) Get(opts ...DBOption) (model.RemoteDB, error) {
var database model.RemoteDB
db := global.DB
for _, opt := range opts {
db = opt(db)
}
err := db.First(&database).Error
return database, err
}
func (u *RemoteDBRepo) Page(page, size int, opts ...DBOption) (int64, []model.RemoteDB, error) {
var users []model.RemoteDB
db := global.DB.Model(&model.RemoteDB{})
for _, opt := range opts {
db = opt(db)
}
count := int64(0)
db = db.Count(&count)
err := db.Limit(size).Offset(size * (page - 1)).Find(&users).Error
return count, users, err
}
func (u *RemoteDBRepo) GetList(opts ...DBOption) ([]model.RemoteDB, error) {
var databases []model.RemoteDB
db := global.DB.Model(&model.RemoteDB{})
for _, opt := range opts {
db = opt(db)
}
err := db.Find(&databases).Error
return databases, err
}
func (c *RemoteDBRepo) WithoutByFrom(from string) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("`from` != ?", from)
}
}
func (u *RemoteDBRepo) Create(database *model.RemoteDB) error {
return global.DB.Create(database).Error
}
func (u *RemoteDBRepo) Update(id uint, vars map[string]interface{}) error {
return global.DB.Model(&model.RemoteDB{}).Where("id = ?", id).Updates(vars).Error
}
func (u *RemoteDBRepo) Delete(opts ...DBOption) error {
db := global.DB
for _, opt := range opts {
db = opt(db)
}
return db.Delete(&model.RemoteDB{}).Error
}

View File

@ -20,6 +20,8 @@ import (
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
"github.com/1Panel-dev/1Panel/backend/utils/common"
"github.com/1Panel-dev/1Panel/backend/utils/compose"
"github.com/1Panel-dev/1Panel/backend/utils/mysql"
"github.com/1Panel-dev/1Panel/backend/utils/mysql/client"
_ "github.com/go-sql-driver/mysql"
"github.com/jinzhu/copier"
"github.com/pkg/errors"
@ -70,50 +72,69 @@ func (u *MysqlService) ListDBName() ([]string, error) {
return dbNames, err
}
var formatMap = map[string]string{
"utf8": "utf8_general_ci",
"utf8mb4": "utf8mb4_general_ci",
"gbk": "gbk_chinese_ci",
"big5": "big5_chinese_ci",
}
func (u *MysqlService) Create(ctx context.Context, req dto.MysqlDBCreate) (*model.DatabaseMysql, error) {
if cmd.CheckIllegal(req.Name, req.Username, req.Password, req.Format, req.Permission) {
return nil, buserr.New(constant.ErrCmdIllegal)
}
if req.Username == "root" {
return nil, errors.New("Cannot set root as user name")
}
app, err := appInstallRepo.LoadBaseInfo("mysql", "")
if err != nil {
return nil, err
}
mysql, _ := mysqlRepo.Get(commonRepo.WithByName(req.Name))
if mysql.ID != 0 {
return nil, constant.ErrRecordExist
}
if err := copier.Copy(&mysql, &req); err != nil {
var createItem model.DatabaseMysql
if err := copier.Copy(&createItem, &req); err != nil {
return nil, errors.WithMessage(constant.ErrStructTransform, err.Error())
}
createSql := fmt.Sprintf("create database `%s` default character set %s collate %s", req.Name, req.Format, formatMap[req.Format])
if err := excSQL(app.ContainerName, app.Password, createSql); err != nil {
if strings.Contains(err.Error(), "ERROR 1007") {
return nil, buserr.New(constant.ErrDatabaseIsExist)
if req.Username == "root" {
return nil, errors.New("Cannot set root as user name")
}
dbInfo := client.DBInfo{
From: req.From,
Timeout: 300,
}
version := ""
if req.From == "local" {
app, err := appInstallRepo.LoadBaseInfo("mysql", "")
if err != nil {
return nil, err
}
mysqlData, _ := mysqlRepo.Get(commonRepo.WithByName(req.Name))
if mysqlData.ID != 0 {
return nil, constant.ErrRecordExist
}
dbInfo.Address = app.ContainerName
dbInfo.Username = "root"
dbInfo.Password = app.Password
version = app.Version
createItem.MysqlName = app.Name
} else {
mysqlData, err := remoteDBRepo.Get(commonRepo.WithByName(req.From))
if err != nil {
return nil, err
}
dbInfo.Address = mysqlData.Address
dbInfo.Port = mysqlData.Port
dbInfo.Username = mysqlData.Username
dbInfo.Password = mysqlData.Password
version = mysqlData.Version
createItem.MysqlName = mysqlData.Name
}
cli, err := mysql.NewMysqlClient(dbInfo)
if err != nil {
return nil, err
}
if err := u.createUser(app.ContainerName, app.Password, app.Version, req); err != nil {
if err := cli.Create(client.CreateInfo{
Name: req.Name,
Format: req.Format,
Version: version,
}); err != nil {
return nil, err
}
global.LOG.Infof("create database %s successful!", req.Name)
mysql.MysqlName = app.Name
if err := mysqlRepo.Create(ctx, &mysql); err != nil {
if err := mysqlRepo.Create(ctx, &createItem); err != nil {
return nil, err
}
return &mysql, nil
return &createItem, nil
}
func (u *MysqlService) UpdateDescription(req dto.UpdateDescription) error {
@ -143,29 +164,28 @@ func (u *MysqlService) DeleteCheck(id uint) ([]string, error) {
}
func (u *MysqlService) Delete(ctx context.Context, req dto.MysqlDBDelete) error {
app, err := appInstallRepo.LoadBaseInfo("mysql", "")
if err != nil && !req.ForceDelete {
cli, version, err := loadClientByID(req.ID)
if err != nil {
return err
}
db, err := mysqlRepo.Get(commonRepo.WithByID(req.ID))
if err != nil && !req.ForceDelete {
return err
}
if strings.HasPrefix(app.Version, "5.6") {
if err := excSQL(app.ContainerName, app.Password, fmt.Sprintf("drop user '%s'@'%s'", db.Username, db.Permission)); err != nil && !req.ForceDelete {
return err
}
} else {
if err := excSQL(app.ContainerName, app.Password, fmt.Sprintf("drop user if exists '%s'@'%s'", db.Username, db.Permission)); err != nil && !req.ForceDelete {
return err
}
}
if err := excSQL(app.ContainerName, app.Password, fmt.Sprintf("drop database if exists `%s`", db.Name)); err != nil && !req.ForceDelete {
if err := cli.Delete(client.DeleteInfo{
Name: db.Name,
Version: version,
Username: db.Username,
Permission: db.Permission,
Timeout: 300,
}); err != nil {
return err
}
app, err := appInstallRepo.LoadBaseInfo("mysql", "")
if err != nil && !req.ForceDelete {
return err
}
global.LOG.Info("execute delete database sql successful, now start to drop uploads and records")
uploadDir := fmt.Sprintf("%s/1panel/uploads/database/mysql/%s/%s", global.CONF.System.BaseDir, app.Name, db.Name)
if _, err := os.Stat(uploadDir); err == nil {
@ -192,66 +212,55 @@ func (u *MysqlService) ChangePassword(info dto.ChangeDBInfo) error {
if cmd.CheckIllegal(info.Value) {
return buserr.New(constant.ErrCmdIllegal)
}
cli, version, err := loadClientByID(info.ID)
if err != nil {
return err
}
var (
mysql model.DatabaseMysql
err error
mysqlData model.DatabaseMysql
passwordInfo client.PasswordChangeInfo
)
passwordInfo.Password = info.Value
passwordInfo.Timeout = 300
passwordInfo.Version = version
if info.ID != 0 {
mysql, err = mysqlRepo.Get(commonRepo.WithByID(info.ID))
mysqlData, err = mysqlRepo.Get(commonRepo.WithByID(info.ID))
if err != nil {
return err
}
passwordInfo.Name = mysqlData.Name
passwordInfo.Username = mysqlData.Username
passwordInfo.Permission = mysqlData.Permission
} else {
passwordInfo.Username = "root"
}
app, err := appInstallRepo.LoadBaseInfo("mysql", "")
if err != nil {
if err := cli.ChangePassword(passwordInfo); err != nil {
return err
}
passwordChangeCMD := fmt.Sprintf("set password for '%s'@'%s' = password('%s')", mysql.Username, mysql.Permission, info.Value)
if !strings.HasPrefix(app.Version, "5.7") && !strings.HasPrefix(app.Version, "5.6") {
passwordChangeCMD = fmt.Sprintf("ALTER USER '%s'@'%s' IDENTIFIED WITH mysql_native_password BY '%s';", mysql.Username, mysql.Permission, info.Value)
}
if info.ID != 0 {
appRess, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithLinkId(app.ID), appInstallResourceRepo.WithResourceId(mysql.ID))
for _, appRes := range appRess {
appInstall, err := appInstallRepo.GetFirst(commonRepo.WithByID(appRes.AppInstallId))
if err != nil {
return err
}
appModel, err := appRepo.GetFirst(commonRepo.WithByID(appInstall.AppId))
if err != nil {
return err
}
// appRess, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithLinkId(app.ID), appInstallResourceRepo.WithResourceId(mysqlData.ID))
// for _, appRes := range appRess {
// appInstall, err := appInstallRepo.GetFirst(commonRepo.WithByID(appRes.AppInstallId))
// if err != nil {
// return err
// }
// appModel, err := appRepo.GetFirst(commonRepo.WithByID(appInstall.AppId))
// if err != nil {
// return err
// }
global.LOG.Infof("start to update mysql password used by app %s-%s", appModel.Key, appInstall.Name)
if err := updateInstallInfoInDB(appModel.Key, appInstall.Name, "user-password", true, info.Value); err != nil {
return err
}
}
if err := excuteSql(app.ContainerName, app.Password, passwordChangeCMD); err != nil {
return err
}
// global.LOG.Infof("start to update mysql password used by app %s-%s", appModel.Key, appInstall.Name)
// if err := updateInstallInfoInDB(appModel.Key, appInstall.Name, "user-password", true, info.Value); err != nil {
// return err
// }
// }
global.LOG.Info("excute password change sql successful")
_ = mysqlRepo.Update(mysql.ID, map[string]interface{}{"password": info.Value})
_ = mysqlRepo.Update(mysqlData.ID, map[string]interface{}{"password": info.Value})
return nil
}
hosts, err := excuteSqlForRows(app.ContainerName, app.Password, "select host from mysql.user where user='root';")
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.Value)
if !strings.HasPrefix(app.Version, "5.7") && !strings.HasPrefix(app.Version, "5.6") {
passwordRootChangeCMD = fmt.Sprintf("alter user 'root'@'%s' identified with mysql_native_password BY '%s';", host, info.Value)
}
if err := excuteSql(app.ContainerName, app.Password, passwordRootChangeCMD); err != nil {
return err
}
}
}
if err := updateInstallInfoInDB("mysql", "", "password", false, info.Value); err != nil {
return err
}
@ -265,71 +274,36 @@ func (u *MysqlService) ChangeAccess(info dto.ChangeDBInfo) error {
if cmd.CheckIllegal(info.Value) {
return buserr.New(constant.ErrCmdIllegal)
}
var (
mysql model.DatabaseMysql
err error
)
if info.ID != 0 {
mysql, err = mysqlRepo.Get(commonRepo.WithByID(info.ID))
if err != nil {
return err
}
if info.Value == mysql.Permission {
return nil
}
}
app, err := appInstallRepo.LoadBaseInfo("mysql", "")
cli, version, err := loadClientByID(info.ID)
if err != nil {
return err
}
if info.ID == 0 {
mysql.Name = "*"
mysql.Username = "root"
mysql.Permission = "%"
mysql.Password = app.Password
}
var (
mysqlData model.DatabaseMysql
accessInfo client.AccessChangeInfo
)
accessInfo.Permission = info.Value
accessInfo.Timeout = 300
accessInfo.Version = version
if info.Value != mysql.Permission {
var userlist []string
if strings.Contains(mysql.Permission, ",") {
userlist = strings.Split(mysql.Permission, ",")
} else {
userlist = append(userlist, mysql.Permission)
}
for _, user := range userlist {
if len(user) != 0 {
if strings.HasPrefix(app.Version, "5.6") {
if err := excuteSql(app.ContainerName, app.Password, fmt.Sprintf("drop user '%s'@'%s'", mysql.Username, user)); err != nil {
return err
}
} else {
if err := excuteSql(app.ContainerName, app.Password, fmt.Sprintf("drop user if exists '%s'@'%s'", mysql.Username, user)); err != nil {
return err
}
}
}
}
if info.ID == 0 {
return nil
if info.ID != 0 {
mysqlData, err = mysqlRepo.Get(commonRepo.WithByID(info.ID))
if err != nil {
return err
}
accessInfo.Name = mysqlData.Name
accessInfo.Username = mysqlData.Username
accessInfo.OldPermission = mysqlData.Permission
} else {
accessInfo.Username = "root"
}
if err := u.createUser(app.ContainerName, app.Password, app.Version, dto.MysqlDBCreate{
Username: mysql.Username,
Name: mysql.Name,
Permission: info.Value,
Password: mysql.Password,
}); err != nil {
if err := cli.ChangeAccess(accessInfo); err != nil {
return err
}
if err := excuteSql(app.ContainerName, app.Password, "flush privileges"); err != nil {
return err
}
if info.ID == 0 {
return nil
}
_ = mysqlRepo.Update(mysql.ID, map[string]interface{}{"permission": info.Value})
if mysqlData.ID != 0 {
_ = mysqlRepo.Update(mysqlData.ID, map[string]interface{}{"permission": info.Value})
}
return nil
}
@ -493,53 +467,6 @@ func (u *MysqlService) LoadStatus() (*dto.MysqlStatus, error) {
return &info, nil
}
func (u *MysqlService) createUser(container, password, version string, req dto.MysqlDBCreate) error {
var userlist []string
if strings.Contains(req.Permission, ",") {
ips := strings.Split(req.Permission, ",")
for _, ip := range ips {
if len(ip) != 0 {
userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", req.Username, ip))
}
}
} else {
userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", req.Username, req.Permission))
}
for _, user := range userlist {
if err := excSQL(container, password, fmt.Sprintf("create user %s identified by '%s';", user, req.Password)); err != nil {
if strings.Contains(err.Error(), "ERROR 1396") {
handleCreateError(container, password, req.Name, userlist, false)
return buserr.New(constant.ErrUserIsExist)
}
handleCreateError(container, password, req.Name, userlist, true)
return err
}
grantStr := fmt.Sprintf("grant all privileges on `%s`.* to %s", req.Name, user)
if req.Name == "*" {
grantStr = fmt.Sprintf("grant all privileges on *.* to %s", user)
}
if strings.HasPrefix(version, "5.7") || strings.HasPrefix(version, "5.6") {
grantStr = fmt.Sprintf("%s identified by '%s' with grant option;", grantStr, req.Password)
}
if err := excSQL(container, password, grantStr); err != nil {
handleCreateError(container, password, req.Name, userlist, true)
return err
}
}
return nil
}
func handleCreateError(contaienr, password, dbName string, userlist []string, dropUser bool) {
_ = excSQL(contaienr, password, fmt.Sprintf("drop database `%s`", dbName))
if dropUser {
for _, user := range userlist {
if err := excSQL(contaienr, password, fmt.Sprintf("drop user if exists %s", user)); err != nil {
global.LOG.Errorf("drop user failed, err: %v", err)
}
}
}
}
func excuteSqlForMaps(containerName, password, command string) (map[string]string, error) {
cmd := exec.Command("docker", "exec", containerName, "mysql", "-uroot", "-p"+password, "-e", command)
stdout, err := cmd.CombinedOutput()
@ -569,31 +496,6 @@ func excuteSqlForRows(containerName, password, command string) ([]string, error)
return strings.Split(stdStr, "\n"), nil
}
func excuteSql(containerName, password, command string) error {
cmd := exec.Command("docker", "exec", containerName, "mysql", "-uroot", "-p"+password, "-e", command)
stdout, err := cmd.CombinedOutput()
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 excSQL(containerName, password, command string) error {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, "docker", "exec", containerName, "mysql", "-uroot", "-p"+password, "-e", command)
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 updateMyCnf(oldFiles []string, group string, param string, value interface{}) []string {
isOn := false
hasGroup := false
@ -634,3 +536,46 @@ func updateMyCnf(oldFiles []string, group string, param string, value interface{
}
return newFiles
}
func loadClientByID(id uint) (mysql.MysqlClient, string, error) {
var (
mysqlData model.DatabaseMysql
dbInfo client.DBInfo
version string
err error
)
if id != 0 {
mysqlData, err = mysqlRepo.Get(commonRepo.WithByID(id))
if err != nil {
return nil, "", err
}
}
if mysqlData.From != "local" {
databaseItem, err := remoteDBRepo.Get(commonRepo.WithByName(mysqlData.From))
if err != nil {
return nil, "", err
}
dbInfo.Address = databaseItem.Address
dbInfo.Port = databaseItem.Port
dbInfo.Username = databaseItem.Username
dbInfo.Password = databaseItem.Password
version = databaseItem.Version
} else {
app, err := appInstallRepo.LoadBaseInfo("mysql", "")
if err != nil {
return nil, "", err
}
dbInfo.Address = app.ContainerName
dbInfo.Username = "root"
dbInfo.Password = app.Password
version = app.Version
}
cli, err := mysql.NewMysqlClient(dbInfo)
if err != nil {
return nil, "", err
}
return cli, version, nil
}

View File

@ -12,7 +12,8 @@ var (
appInstallRepo = repo.NewIAppInstallRepo()
appInstallResourceRepo = repo.NewIAppInstallResourceRpo()
mysqlRepo = repo.NewIMysqlRepo()
mysqlRepo = repo.NewIMysqlRepo()
remoteDBRepo = repo.NewIRemoteDBRepo()
imageRepoRepo = repo.NewIImageRepoRepo()
composeRepo = repo.NewIComposeTemplateRepo()

View File

@ -0,0 +1,108 @@
package service
import (
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/utils/mysql"
"github.com/1Panel-dev/1Panel/backend/utils/mysql/client"
"github.com/jinzhu/copier"
"github.com/pkg/errors"
)
type RemoteDBService struct{}
type IRemoteDBService interface {
SearchWithPage(search dto.RemoteDBSearch) (int64, interface{}, error)
Create(req dto.RemoteDBCreate) error
Update(req dto.RemoteDBUpdate) error
Delete(id uint) error
List(dbType string) ([]dto.RemoteDBOption, error)
}
func NewIRemoteDBService() IRemoteDBService {
return &RemoteDBService{}
}
func (u *RemoteDBService) SearchWithPage(search dto.RemoteDBSearch) (int64, interface{}, error) {
total, dbs, err := remoteDBRepo.Page(search.Page, search.PageSize,
commonRepo.WithByType(search.Type),
commonRepo.WithLikeName(search.Info),
remoteDBRepo.WithoutByFrom("local"),
)
var datas []dto.RemoteDBInfo
for _, db := range dbs {
var item dto.RemoteDBInfo
if err := copier.Copy(&item, &db); err != nil {
return 0, nil, errors.WithMessage(constant.ErrStructTransform, err.Error())
}
datas = append(datas, item)
}
return total, datas, err
}
func (u *RemoteDBService) List(dbType string) ([]dto.RemoteDBOption, error) {
dbs, err := remoteDBRepo.GetList(commonRepo.WithByType(dbType), remoteDBRepo.WithoutByFrom("local"))
var datas []dto.RemoteDBOption
for _, db := range dbs {
var item dto.RemoteDBOption
if err := copier.Copy(&item, &db); err != nil {
return nil, errors.WithMessage(constant.ErrStructTransform, err.Error())
}
datas = append(datas, item)
}
return datas, err
}
func (u *RemoteDBService) Create(req dto.RemoteDBCreate) error {
db, _ := remoteDBRepo.Get(commonRepo.WithByName(req.Name))
if db.ID != 0 {
return constant.ErrRecordExist
}
if _, err := mysql.NewMysqlClient(client.DBInfo{
From: "remote",
Address: req.Address,
Port: req.Port,
Username: req.Username,
Password: req.Password,
Timeout: 300,
}); err != nil {
return err
}
if err := copier.Copy(&db, &req); err != nil {
return errors.WithMessage(constant.ErrStructTransform, err.Error())
}
if err := remoteDBRepo.Create(&db); err != nil {
return err
}
return nil
}
func (u *RemoteDBService) Delete(id uint) error {
db, _ := remoteDBRepo.Get(commonRepo.WithByID(id))
if db.ID == 0 {
return constant.ErrRecordNotFound
}
return remoteDBRepo.Delete(commonRepo.WithByID(id))
}
func (u *RemoteDBService) Update(req dto.RemoteDBUpdate) error {
if _, err := mysql.NewMysqlClient(client.DBInfo{
From: "remote",
Address: req.Address,
Port: req.Port,
Username: req.Username,
Password: req.Password,
Timeout: 300,
}); err != nil {
return err
}
upMap := make(map[string]interface{})
upMap["version"] = req.Version
upMap["address"] = req.Address
upMap["port"] = req.Port
upMap["username"] = req.Username
upMap["password"] = req.Password
upMap["description"] = req.Description
return remoteDBRepo.Update(req.ID, upMap)
}

View File

@ -481,9 +481,9 @@ var EncryptHostPassword = &gormigrate.Migration{
}
var AddRemoteDB = &gormigrate.Migration{
ID: "20230718-add-remote-db",
ID: "20230720-add-remote-db",
Migrate: func(tx *gorm.DB) error {
if err := tx.AutoMigrate(&model.Database{}); err != nil {
if err := tx.AutoMigrate(&model.RemoteDB{}); err != nil {
return err
}
return nil

View File

@ -40,5 +40,11 @@ func (s *DatabaseRouter) InitDatabaseRouter(Router *gin.RouterGroup) {
cmdRouter.POST("/redis/conf/update", baseApi.UpdateRedisConf)
cmdRouter.POST("/redis/conffile/update", baseApi.UpdateRedisConfByFile)
cmdRouter.POST("/redis/persistence/update", baseApi.UpdateRedisPersistenceConf)
cmdRouter.POST("/remote", baseApi.CreateRemoteDB)
cmdRouter.POST("/remote/list/:type", baseApi.ListRemoteDB)
cmdRouter.POST("/remote/update", baseApi.UpdateRemoteDB)
cmdRouter.POST("/remote/search", baseApi.SearchRemoteDB)
cmdRouter.POST("/remote/del", baseApi.DeleteRemoteDB)
}
}

View File

@ -22,19 +22,22 @@ type MysqlClient interface {
}
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)
if conn.From == "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
}
if err := db.Ping(); err != nil {
return nil, err
}
return client.NewRemote(db), nil
}
if conn.Type == "local" {
if cmd.CheckIllegal(conn.Address, conn.UserName, conn.Password) {
if conn.From == "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"}
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")

View File

@ -1,12 +1,11 @@
package client
type DBInfo struct {
Type string `json:"type"` // local remote
From string `json:"from"` // local remote
Address string `json:"address"`
Port uint `json:"port"`
UserName string `json:"userName"`
Username string `json:"userName"`
Password string `json:"password"`
Format string `json:"format"`
Timeout uint `json:"timeout"` // second
}
@ -15,7 +14,7 @@ type CreateInfo struct {
Name string `json:"name"`
Format string `json:"format"`
Version string `json:"version"`
UserName string `json:"userName"`
Username string `json:"userName"`
Password string `json:"password"`
Permission string `json:"permission"`
@ -25,7 +24,7 @@ type CreateInfo struct {
type DeleteInfo struct {
Name string `json:"name"`
Version string `json:"version"`
UserName string `json:"userName"`
Username string `json:"userName"`
Permission string `json:"permission"`
ForceDelete bool `json:"forceDelete"`
@ -35,7 +34,7 @@ type DeleteInfo struct {
type PasswordChangeInfo struct {
Name string `json:"name"`
Version string `json:"version"`
UserName string `json:"userName"`
Username string `json:"userName"`
Password string `json:"password"`
Permission string `json:"permission"`
@ -45,7 +44,7 @@ type PasswordChangeInfo struct {
type AccessChangeInfo struct {
Name string `json:"name"`
Version string `json:"version"`
UserName string `json:"userName"`
Username string `json:"userName"`
OldPermission string `json:"oldPermission"`
Permission string `json:"permission"`

View File

@ -31,7 +31,7 @@ func (r *Local) Create(info CreateInfo) error {
return err
}
if err := r.CreateUser(CreateInfo{Name: info.Name, Version: info.Version, UserName: info.UserName, Permission: info.Permission, Timeout: info.Timeout}); err != 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
}
@ -44,11 +44,11 @@ func (r *Local) CreateUser(info CreateInfo) error {
ips := strings.Split(info.Permission, ",")
for _, ip := range ips {
if len(ip) != 0 {
userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.UserName, ip))
userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.Username, ip))
}
}
} else {
userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.UserName, info.Permission))
userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.Username, info.Permission))
}
for _, user := range userlist {
@ -56,7 +56,7 @@ func (r *Local) CreateUser(info CreateInfo) error {
_ = r.Delete(DeleteInfo{
Name: info.Name,
Version: info.Version,
UserName: info.UserName,
Username: info.Username,
Permission: info.Permission,
ForceDelete: true,
Timeout: 300})
@ -76,7 +76,7 @@ func (r *Local) CreateUser(info CreateInfo) error {
_ = r.Delete(DeleteInfo{
Name: info.Name,
Version: info.Version,
UserName: info.UserName,
Username: info.Username,
Permission: info.Permission,
ForceDelete: true,
Timeout: 300})
@ -92,11 +92,11 @@ func (r *Local) Delete(info DeleteInfo) error {
ips := strings.Split(info.Permission, ",")
for _, ip := range ips {
if len(ip) != 0 {
userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.UserName, ip))
userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.Username, ip))
}
}
} else {
userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.UserName, info.Permission))
userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.Username, info.Permission))
}
for _, user := range userlist {
@ -123,17 +123,17 @@ func (r *Local) Delete(info DeleteInfo) error {
}
func (r *Local) ChangePassword(info PasswordChangeInfo) error {
if info.UserName != "root" {
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))
userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.Username, ip))
}
}
} else {
userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.UserName, info.Permission))
userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.Username, info.Permission))
}
for _, user := range userlist {
@ -168,24 +168,27 @@ func (r *Local) ChangePassword(info PasswordChangeInfo) error {
}
func (r *Local) ChangeAccess(info AccessChangeInfo) error {
if info.UserName == "root" {
if info.Username == "root" {
info.OldPermission = "%"
info.Name = "*"
}
if info.Permission != info.OldPermission {
if err := r.Delete(DeleteInfo{
Version: info.Version,
UserName: info.UserName,
Username: info.Username,
Permission: info.OldPermission,
ForceDelete: true,
Timeout: 300}); err != nil {
return err
}
if info.UserName == "root" {
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 {
if err := r.CreateUser(CreateInfo{Name: info.Name, Version: info.Version, Username: info.Username, Permission: info.Permission, Timeout: info.Timeout}); err != nil {
return err
}
if err := r.ExecSQL("flush privileges", 300); err != nil {
return err
}
return nil

View File

@ -29,7 +29,7 @@ func (r *Remote) Create(info CreateInfo) error {
return err
}
if err := r.CreateUser(CreateInfo{Name: info.Name, Version: info.Version, UserName: info.UserName, Permission: info.Permission, Timeout: info.Timeout}); err != 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
}
@ -42,11 +42,11 @@ func (r *Remote) CreateUser(info CreateInfo) error {
ips := strings.Split(info.Permission, ",")
for _, ip := range ips {
if len(ip) != 0 {
userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.UserName, ip))
userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.Username, ip))
}
}
} else {
userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.UserName, info.Permission))
userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.Username, info.Permission))
}
for _, user := range userlist {
@ -54,7 +54,7 @@ func (r *Remote) CreateUser(info CreateInfo) error {
_ = r.Delete(DeleteInfo{
Name: info.Name,
Version: info.Version,
UserName: info.UserName,
Username: info.Username,
Permission: info.Permission,
ForceDelete: true,
Timeout: 300})
@ -74,7 +74,7 @@ func (r *Remote) CreateUser(info CreateInfo) error {
_ = r.Delete(DeleteInfo{
Name: info.Name,
Version: info.Version,
UserName: info.UserName,
Username: info.Username,
Permission: info.Permission,
ForceDelete: true,
Timeout: 300})
@ -90,11 +90,11 @@ func (r *Remote) Delete(info DeleteInfo) error {
ips := strings.Split(info.Permission, ",")
for _, ip := range ips {
if len(ip) != 0 {
userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.UserName, ip))
userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.Username, ip))
}
}
} else {
userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.UserName, info.Permission))
userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.Username, info.Permission))
}
for _, user := range userlist {
@ -121,17 +121,17 @@ func (r *Remote) Delete(info DeleteInfo) error {
}
func (r *Remote) ChangePassword(info PasswordChangeInfo) error {
if info.UserName != "root" {
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))
userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.Username, ip))
}
}
} else {
userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.UserName, info.Permission))
userlist = append(userlist, fmt.Sprintf("'%s'@'%s'", info.Username, info.Permission))
}
for _, user := range userlist {
@ -166,24 +166,27 @@ func (r *Remote) ChangePassword(info PasswordChangeInfo) error {
}
func (r *Remote) ChangeAccess(info AccessChangeInfo) error {
if info.UserName == "root" {
if info.Username == "root" {
info.OldPermission = "%"
info.Name = "*"
}
if info.Permission != info.OldPermission {
if err := r.Delete(DeleteInfo{
Version: info.Version,
UserName: info.UserName,
Username: info.Username,
Permission: info.OldPermission,
ForceDelete: true,
Timeout: 300}); err != nil {
return err
}
if info.UserName == "root" {
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 {
if err := r.CreateUser(CreateInfo{Name: info.Name, Version: info.Version, Username: info.Username, Permission: info.Permission, Timeout: info.Timeout}); err != nil {
return err
}
if err := r.ExecSQL("flush privileges", 300); err != nil {
return err
}
return nil

View File

@ -131,6 +131,7 @@ export namespace App {
export interface DatabaseConnInfo {
password: string;
privilege: boolean;
serviceName: string;
port: number;
}

View File

@ -167,6 +167,40 @@ export namespace Database {
createdAt: Date;
name: string;
type: string;
version: string;
from: string;
address: string;
port: number;
username: string;
password: string;
description: string;
}
export interface SearchRemoteDBPage {
info: string;
type: string;
page: number;
pageSize: number;
orderBy?: string;
order?: string;
}
export interface RemoteDBOption {
id: number;
name: string;
address: string;
}
export interface RemoteDBCreate {
name: string;
version: string;
from: string;
address: string;
port: number;
username: string;
password: string;
description: string;
}
export interface RemoteDBUpdate {
id: number;
version: string;
address: string;
port: number;
username: string;

View File

@ -85,13 +85,16 @@ export const updateRedisConfByFile = (params: Database.RedisConfUpdateByFile) =>
};
// remote
export const searchRemoteDBs = (params: SearchWithPage) => {
export const searchRemoteDBs = (params: Database.SearchRemoteDBPage) => {
return http.post<ResPage<Database.RemoteDBInfo>>(`/databases/remote/search`, params);
};
export const addRemoteDB = (params: Database.RemoteDBInfo) => {
export const listRemoteDBs = (type: string) => {
return http.get<Array<Database.RemoteDBOption>>(`/databases/remote/list/${type}`);
};
export const addRemoteDB = (params: Database.RemoteDBCreate) => {
return http.post(`/databases/remote`, params);
};
export const editRemoteDB = (params: Database.RemoteDBInfo) => {
export const editRemoteDB = (params: Database.RemoteDBUpdate) => {
return http.post(`/databases/remote/update`, params);
};
export const deleteRemoteDB = (id: number) => {

View File

@ -330,6 +330,13 @@ const message = {
confChange: '配置修改',
remoteDB: '远程数据库',
createRemoteDB: '创建远程数据库',
editRemoteDB: '编辑远程数据库',
localDB: '本地数据库',
address: '数据库地址',
version: '数据库版本',
selectFile: '选择文件',
dropHelper: '将上传文件拖拽到此处或者',
clickHelper: '点击上传',

View File

@ -27,6 +27,16 @@ const databaseRouter = {
requiresAuth: false,
},
},
{
path: 'mysql/remote',
name: 'MySQL-Remote',
component: () => import('@/views/database/mysql/remote/index.vue'),
hidden: true,
meta: {
activeMenu: '/databases',
requiresAuth: false,
},
},
{
path: 'redis',
name: 'Redis',

View File

@ -20,8 +20,8 @@
<el-button @click="onChangeRootPassword" type="primary" plain>
{{ $t('database.databaseConnInfo') }}
</el-button>
<el-button @click="onChangeAccess" type="primary" plain>
{{ $t('database.remoteAccess') }}
<el-button @click="goRemoteDB" type="primary" plain>
{{ $t('database.remoteDB') }}
</el-button>
<el-button @click="goDashboard" icon="Position" type="primary" plain>phpMyAdmin</el-button>
</el-col>
@ -134,7 +134,6 @@
<PasswordDialog ref="passwordRef" @search="search" />
<RootPasswordDialog ref="rootPasswordRef" />
<RemoteAccessDialog ref="remoteAccessRef" />
<UploadDialog ref="uploadRef" />
<OperateDialog @search="search" ref="dialogRef" />
<Backups ref="dialogBackupRef" />
@ -151,7 +150,6 @@ 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';
import RootPasswordDialog from '@/views/database/mysql/root-password/index.vue';
import RemoteAccessDialog from '@/views/database/mysql/remote/index.vue';
import AppResources from '@/views/database/mysql/check/index.vue';
import Setting from '@/views/database/mysql/setting/index.vue';
import AppStatus from '@/components/app-status/index.vue';
@ -160,7 +158,7 @@ import UploadDialog from '@/components/upload/index.vue';
import PortJumpDialog from '@/components/port-jump/index.vue';
import { dateFormat } from '@/utils/util';
import { reactive, ref } from 'vue';
import { deleteCheckMysqlDB, loadRemoteAccess, searchMysqlDBs, updateMysqlDescription } from '@/api/modules/database';
import { deleteCheckMysqlDB, searchMysqlDBs, updateMysqlDescription } from '@/api/modules/database';
import i18n from '@/lang';
import { Database } from '@/api/interface/database';
import { App } from '@/api/interface/app';
@ -222,13 +220,8 @@ const onChangeRootPassword = async () => {
rootPasswordRef.value!.acceptParams();
};
const remoteAccessRef = ref();
const onChangeAccess = async () => {
const res = await loadRemoteAccess();
let param = {
privilege: res.data,
};
remoteAccessRef.value!.acceptParams(param);
const goRemoteDB = async () => {
router.push({ name: 'MySQL-Remote' });
};
const passwordRef = ref();

View File

@ -1,98 +1,181 @@
<template>
<el-drawer v-model="dialogVisiable" :destroy-on-close="true" :close-on-click-modal="false" size="30%">
<template #header>
<DrawerHeader :header="$t('database.remoteAccess')" :back="handleClose" />
</template>
<el-form @submit.prevent v-loading="loading" ref="formRef" :model="form" label-position="top">
<el-row type="flex" justify="center">
<el-col :span="22">
<el-form-item :label="$t('database.remoteAccess')" :rules="Rules.requiredInput" prop="privilege">
<el-switch v-model="form.privilege" />
<span class="input-help">{{ $t('database.remoteConnHelper') }}</span>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div v-loading="loading">
<LayoutContent>
<template #title>
<back-button name="MySQL" :header="'MySQL ' + $t('database.remoteDB')" />
</template>
<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.createRemoteDB') }}
</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('database.address')" 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="description"
:label="$t('commons.table.description')"
show-overflow-tooltip
/>
<el-table-column
prop="createdAt"
:label="$t('commons.table.date')"
:formatter="dateFormat"
show-overflow-tooltip
/>
<fu-table-operations
width="370px"
:buttons="buttons"
:ellipsis="10"
:label="$t('commons.table.operate')"
fix
/>
</ComplexTable>
</template>
</LayoutContent>
<ConfirmDialog ref="confirmDialogRef" @confirm="onSubmit"></ConfirmDialog>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
<el-button type="primary" @click="onSave(formRef)">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</el-drawer>
<OperateDialog ref="dialogRef" @search="search" />
</div>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { Rules } from '@/global/form-rules';
import { dateFormat } from '@/utils/util';
import { onMounted, reactive, ref } from 'vue';
import { deleteRemoteDB, searchRemoteDBs } from '@/api/modules/database';
import OperateDialog from '@/views/database/mysql/remote/operate/index.vue';
import i18n from '@/lang';
import { ElForm } from 'element-plus';
import { updateMysqlAccess } from '@/api/modules/database';
import ConfirmDialog from '@/components/confirm-dialog/index.vue';
import DrawerHeader from '@/components/drawer-header/index.vue';
import { MsgSuccess } from '@/utils/message';
import { MsgError, MsgSuccess } from '@/utils/message';
import useClipboard from 'vue-clipboard3';
import { Database } from '@/api/interface/database';
import { useDeleteData } from '@/hooks/use-delete-data';
const { toClipboard } = useClipboard();
const loading = ref(false);
const dialogVisiable = ref(false);
const form = reactive({
privilege: false,
const dialogRef = ref();
const data = ref();
const paginationConfig = reactive({
currentPage: 1,
pageSize: 10,
total: 0,
});
const searchName = ref();
const confirmDialogRef = ref();
type FormInstance = InstanceType<typeof ElForm>;
const formRef = ref<FormInstance>();
interface DialogProps {
privilege: boolean;
}
const acceptParams = (prop: DialogProps): void => {
form.privilege = prop.privilege;
dialogVisiable.value = true;
};
const handleClose = () => {
dialogVisiable.value = false;
};
const onSubmit = async () => {
let param = {
id: 0,
value: form.privilege ? '%' : 'localhost',
const search = async (column?: any) => {
let params = {
page: paginationConfig.currentPage,
pageSize: paginationConfig.pageSize,
info: searchName.value,
type: 'mysql',
orderBy: column?.order ? column.prop : 'created_at',
order: column?.order ? column.order : 'null',
};
loading.value = true;
await updateMysqlAccess(param)
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
dialogVisiable.value = false;
})
.catch(() => {
loading.value = false;
});
const res = await searchRemoteDBs(params);
data.value = res.data.items || [];
paginationConfig.total = res.data.total;
};
const onSave = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
let params = {
header: i18n.global.t('database.confChange'),
operationInfo: i18n.global.t('database.restartNowHelper'),
submitInputInfo: i18n.global.t('database.restartNow'),
};
confirmDialogRef.value!.acceptParams(params);
});
const onOpenDialog = async (
title: string,
rowData: Partial<Database.RemoteDBInfo> = {
name: '',
type: 'mysql',
version: '5.6.x',
address: '',
port: 3306,
username: '',
password: '',
description: '',
},
) => {
let params = {
title,
rowData: { ...rowData },
};
dialogRef.value!.acceptParams(params);
};
defineExpose({
acceptParams,
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.RemoteDBInfo) => {
await useDeleteData(deleteRemoteDB, row.id, 'commons.msg.delete');
search();
};
const buttons = [
{
label: i18n.global.t('commons.button.edit'),
click: (row: Database.RemoteDBInfo) => {
onOpenDialog('edit', row);
},
},
{
label: i18n.global.t('commons.button.delete'),
click: (row: Database.RemoteDBInfo) => {
onDelete(row);
},
},
];
onMounted(() => {
search();
});
</script>

View File

@ -6,33 +6,33 @@
<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 :label="$t('commons.table.name')" prop="name">
<el-input 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-form-item :label="$t('database.version')" prop="version">
<el-select v-model="dialogData.rowData!.version">
<el-option value="5.6.x" label="5.6.x" />
<el-option value="5.7.x" label="5.7.x" />
<el-option value="8.0.x" label="8.0.x" />
</el-select>
</el-form-item>
<el-form-item :label="$t('cronjob.taskName')" prop="address">
<el-form-item :label="$t('database.address')" 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 :label="$t('commons.table.port')" prop="port">
<el-input clearable v-model.trim="dialogData.rowData!.port" />
</el-form-item>
<el-form-item :label="$t('cronjob.taskName')" prop="username">
<el-form-item :label="$t('commons.login.username')" 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 :label="$t('commons.login.password')" prop="password">
<el-input type="password" clearable show-password v-model.trim="dialogData.rowData!.password">
<template #append>
<el-button @click="random">{{ $t('commons.button.random') }}</el-button>
</template>
</el-input>
</el-form-item>
<el-form-item :label="$t('cronjob.description')" prop="description">
<el-form-item :label="$t('commons.table.description')" prop="description">
<el-input clearable v-model.trim="dialogData.rowData!.description" />
</el-form-item>
</el-col>
@ -53,14 +53,16 @@
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 { Database } from '@/api/interface/database';
import DrawerHeader from '@/components/drawer-header/index.vue';
import { MsgSuccess } from '@/utils/message';
import { Rules } from '@/global/form-rules';
import { getRandomStr } from '@/utils/util';
import { addRemoteDB, editRemoteDB } from '@/api/modules/database';
interface DialogProps {
title: string;
rowData?: Cronjob.CronjobInfo;
rowData?: Database.RemoteDBInfo;
getTableList?: () => Promise<any>;
}
const title = ref<string>('');
@ -70,7 +72,7 @@ const dialogData = ref<DialogProps>({
});
const acceptParams = (params: DialogProps): void => {
dialogData.value = params;
title.value = i18n.global.t('cronjob.' + dialogData.value.title);
title.value = i18n.global.t('database.' + dialogData.value.title + 'RemoteDB');
drawerVisiable.value = true;
};
const emit = defineEmits<{ (e: 'search'): void }>();
@ -81,23 +83,49 @@ const handleClose = () => {
const rules = reactive({
name: [Rules.requiredInput],
type: [Rules.requiredSelect],
address: [Rules.requiredInput],
version: [Rules.requiredSelect],
address: [Rules.ip],
port: [Rules.port],
username: [Rules.requiredInput],
password: [Rules.requiredInput],
});
type FormInstance = InstanceType<typeof ElForm>;
const formRef = ref<FormInstance>();
const random = async () => {
dialogData.value.rowData!.password = getRandomStr(16);
};
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);
let param = {
name: dialogData.value.rowData.name,
type: 'mysql',
version: dialogData.value.rowData.version,
from: 'remote',
address: dialogData.value.rowData.address,
port: dialogData.value.rowData.port,
username: dialogData.value.rowData.username,
password: dialogData.value.rowData.password,
description: dialogData.value.rowData.description,
};
await addRemoteDB(param);
}
if (dialogData.value.title === 'edit') {
await editCronjob(dialogData.value.rowData);
let param = {
id: dialogData.value.rowData.id,
version: dialogData.value.rowData.version,
address: dialogData.value.rowData.address,
port: dialogData.value.rowData.port,
username: dialogData.value.rowData.username,
password: dialogData.value.rowData.password,
description: dialogData.value.rowData.description,
};
await editRemoteDB(param);
}
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));

View File

@ -6,22 +6,6 @@
<el-form @submit.prevent v-loading="loading" ref="formRef" :model="form" label-position="top">
<el-row type="flex" justify="center">
<el-col :span="22">
<el-form-item :label="$t('database.rootPassword')" :rules="Rules.requiredInput" prop="password">
<el-input type="password" show-password clearable v-model="form.password">
<template #append>
<el-button @click="onCopy(form.password)">{{ $t('commons.button.copy') }}</el-button>
<el-divider direction="vertical" />
<el-button @click="random">
{{ $t('commons.button.random') }}
</el-button>
</template>
</el-input>
</el-form-item>
<el-form-item :label="$t('database.serviceName')" prop="serviceName">
<el-tag>{{ form.serviceName }}</el-tag>
<el-button @click="onCopy(form.serviceName)" icon="DocumentCopy" link></el-button>
<span class="input-help">{{ $t('database.serviceNameHelper') }}</span>
</el-form-item>
<el-form-item :label="$t('database.containerConn')">
<el-tag>
{{ form.serviceName + ':3306' }}
@ -35,11 +19,30 @@
<el-tag>{{ $t('database.localIP') + ':' + form.port }}</el-tag>
<span class="input-help">{{ $t('database.remoteConnHelper2') }}</span>
</el-form-item>
<el-divider border-style="dashed" />
<el-form-item :label="$t('database.remoteAccess')" prop="privilege">
<el-switch v-model="form.privilege" @change="onSaveAccess" />
<span class="input-help">{{ $t('database.remoteConnHelper') }}</span>
</el-form-item>
<el-form-item :label="$t('database.rootPassword')" :rules="Rules.requiredInput" prop="password">
<el-input type="password" show-password clearable v-model="form.password">
<template #append>
<el-button @click="onCopy(form.password)">{{ $t('commons.button.copy') }}</el-button>
<el-divider direction="vertical" />
<el-button @click="random">
{{ $t('commons.button.random') }}
</el-button>
</template>
</el-input>
</el-form-item>
</el-col>
</el-row>
</el-form>
<ConfirmDialog ref="confirmDialogRef" @confirm="onSubmit"></ConfirmDialog>
<ConfirmDialog ref="confirmDialogRef" @confirm="onSubmit" @cancel="loadPassword"></ConfirmDialog>
<ConfirmDialog ref="confirmAccessDialogRef" @confirm="onSubmitAccess" @cancel="loadAccess"></ConfirmDialog>
<template #footer>
<span class="dialog-footer">
@ -59,7 +62,7 @@ import { ref } from 'vue';
import { Rules } from '@/global/form-rules';
import i18n from '@/lang';
import { ElForm } from 'element-plus';
import { updateMysqlPassword } from '@/api/modules/database';
import { loadRemoteAccess, updateMysqlAccess, updateMysqlPassword } from '@/api/modules/database';
import ConfirmDialog from '@/components/confirm-dialog/index.vue';
import { GetAppConnInfo } from '@/api/modules/app';
import DrawerHeader from '@/components/drawer-header/index.vue';
@ -75,10 +78,12 @@ const dialogVisiable = ref(false);
const form = ref<App.DatabaseConnInfo>({
password: '',
serviceName: '',
privilege: false,
port: 0,
});
const confirmDialogRef = ref();
const confirmAccessDialogRef = ref();
type FormInstance = InstanceType<typeof ElForm>;
const formRef = ref<FormInstance>();
@ -86,6 +91,7 @@ const formRef = ref<FormInstance>();
const acceptParams = (): void => {
form.value.password = '';
loadPassword();
loadAccess();
dialogVisiable.value = true;
};
@ -106,6 +112,11 @@ const handleClose = () => {
dialogVisiable.value = false;
};
const loadAccess = async () => {
const res = await loadRemoteAccess();
form.value.privilege = res.data;
};
const loadPassword = async () => {
const res = await GetAppConnInfo('mysql');
form.value = res.data;
@ -141,6 +152,32 @@ const onSave = async (formEl: FormInstance | undefined) => {
});
};
const onSubmitAccess = async () => {
let param = {
id: 0,
value: form.value.privilege ? '%' : 'localhost',
};
loading.value = true;
await updateMysqlAccess(param)
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
dialogVisiable.value = false;
})
.catch(() => {
loading.value = false;
});
};
const onSaveAccess = () => {
let params = {
header: i18n.global.t('database.confChange'),
operationInfo: i18n.global.t('database.restartNowHelper'),
submitInputInfo: i18n.global.t('database.restartNow'),
};
confirmAccessDialogRef.value!.acceptParams(params);
};
defineExpose({
acceptParams,
});

View File

@ -1,152 +0,0 @@
<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>