mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-09-22 00:00:50 +00:00
feat(database): improve MySQL user authorization (#13326)
This commit is contained in:
@@ -161,6 +161,35 @@ func (b *BaseApi) ChangeMysqlUserPassword(c *gin.Context) {
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Database Mysql
|
||||
// @Summary Save mysql user password locally
|
||||
// @Accept json
|
||||
// @Param request body dto.MysqlUserPassword true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /databases/users/password/save [post]
|
||||
// @x-panel-log {"bodyKeys":["database","username","host"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"补充 mysql 数据库 [database] 用户 [username]@[host] 密码","formatEN":"save mysql database [database] user [username]@[host] password locally"}
|
||||
func (b *BaseApi) SaveMysqlUserPassword(c *gin.Context) {
|
||||
var req dto.MysqlUserPassword
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if len(req.Password) != 0 {
|
||||
password, err := base64.StdEncoding.DecodeString(req.Password)
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
req.Password = string(password)
|
||||
}
|
||||
if err := mysqlService.SaveUserPassword(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Database Mysql
|
||||
// @Summary List mysql grants
|
||||
// @Accept json
|
||||
|
||||
@@ -71,6 +71,7 @@ type MysqlUser struct {
|
||||
Host string `json:"host"`
|
||||
Password string `json:"password"`
|
||||
Description string `json:"description"`
|
||||
IsDelete bool `json:"isDelete"`
|
||||
}
|
||||
|
||||
type MysqlGrant struct {
|
||||
|
||||
@@ -10,3 +10,12 @@ type DatabaseUser struct {
|
||||
Description string `json:"description"`
|
||||
IsDelete bool `json:"isDelete"`
|
||||
}
|
||||
|
||||
type DatabaseUserGrant struct {
|
||||
BaseModel
|
||||
Type string `json:"type" gorm:"not null;uniqueIndex:idx_database_user_grant"`
|
||||
Database string `json:"database" gorm:"not null;uniqueIndex:idx_database_user_grant"`
|
||||
DBName string `json:"dbName" gorm:"not null;uniqueIndex:idx_database_user_grant"`
|
||||
Username string `json:"username" gorm:"not null;uniqueIndex:idx_database_user_grant"`
|
||||
Host string `json:"host" gorm:"not null;uniqueIndex:idx_database_user_grant"`
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ type IAppInstallResourceRpo interface {
|
||||
WithAppInstallId(appInstallId uint) DBOption
|
||||
WithLinkId(linkId uint) DBOption
|
||||
WithResourceId(resourceId uint) DBOption
|
||||
WithResourceIds(resourceIds []uint) DBOption
|
||||
GetBy(opts ...DBOption) ([]model.AppInstallResource, error)
|
||||
GetFirst(opts ...DBOption) (model.AppInstallResource, error)
|
||||
Create(ctx context.Context, resource *model.AppInstallResource) error
|
||||
@@ -44,6 +45,12 @@ func (a AppInstallResourceRpo) WithResourceId(resourceId uint) DBOption {
|
||||
}
|
||||
}
|
||||
|
||||
func (a AppInstallResourceRpo) WithResourceIds(resourceIds []uint) DBOption {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
return db.Where("resource_id IN ?", resourceIds)
|
||||
}
|
||||
}
|
||||
|
||||
func (a AppInstallResourceRpo) GetBy(opts ...DBOption) ([]model.AppInstallResource, error) {
|
||||
db := global.DB.Model(&model.AppInstallResource{})
|
||||
var resources []model.AppInstallResource
|
||||
|
||||
@@ -19,6 +19,7 @@ type IDatabaseUserRepo interface {
|
||||
Update(vars map[string]interface{}, opts ...DBOption) error
|
||||
WithByDatabase(database string) DBOption
|
||||
WithByUser(username, host string) DBOption
|
||||
WithByUserList(users [][2]string) DBOption
|
||||
}
|
||||
|
||||
func NewIDatabaseUserRepo() IDatabaseUserRepo {
|
||||
@@ -99,3 +100,16 @@ func (u *DatabaseUserRepo) WithByUser(username, host string) DBOption {
|
||||
return g.Where("username = ? AND host = ?", username, host)
|
||||
}
|
||||
}
|
||||
|
||||
func (u *DatabaseUserRepo) WithByUserList(users [][2]string) DBOption {
|
||||
return func(g *gorm.DB) *gorm.DB {
|
||||
if len(users) == 0 {
|
||||
return g.Where("1 = 0")
|
||||
}
|
||||
values := make([][]interface{}, 0, len(users))
|
||||
for _, user := range users {
|
||||
values = append(values, []interface{}{user[0], user[1]})
|
||||
}
|
||||
return g.Where("(username, host) IN ?", values)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type DatabaseUserGrantRepo struct{}
|
||||
|
||||
type IDatabaseUserGrantRepo interface {
|
||||
Get(opts ...DBOption) (model.DatabaseUserGrant, error)
|
||||
List(opts ...DBOption) ([]model.DatabaseUserGrant, error)
|
||||
Save(grant *model.DatabaseUserGrant) error
|
||||
Replace(dbType, database string, grants []model.DatabaseUserGrant) error
|
||||
Delete(opts ...DBOption) error
|
||||
Update(vars map[string]interface{}, opts ...DBOption) error
|
||||
WithByDatabase(database string) DBOption
|
||||
WithByDBName(dbName string) DBOption
|
||||
WithByDBNames(dbNames []string) DBOption
|
||||
WithByUser(username, host string) DBOption
|
||||
}
|
||||
|
||||
func (u *DatabaseUserGrantRepo) Get(opts ...DBOption) (model.DatabaseUserGrant, error) {
|
||||
var grant model.DatabaseUserGrant
|
||||
db := global.DB.Model(&model.DatabaseUserGrant{})
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
err := db.First(&grant).Error
|
||||
return grant, err
|
||||
}
|
||||
|
||||
func NewIDatabaseUserGrantRepo() IDatabaseUserGrantRepo {
|
||||
return &DatabaseUserGrantRepo{}
|
||||
}
|
||||
|
||||
func (u *DatabaseUserGrantRepo) List(opts ...DBOption) ([]model.DatabaseUserGrant, error) {
|
||||
var grants []model.DatabaseUserGrant
|
||||
db := global.DB.Model(&model.DatabaseUserGrant{})
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
err := db.Find(&grants).Error
|
||||
return grants, err
|
||||
}
|
||||
|
||||
func (u *DatabaseUserGrantRepo) Save(grant *model.DatabaseUserGrant) error {
|
||||
return global.DB.Save(grant).Error
|
||||
}
|
||||
|
||||
func (u *DatabaseUserGrantRepo) Replace(dbType, database string, grants []model.DatabaseUserGrant) error {
|
||||
return global.DB.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Where("`type` = ? AND database = ?", dbType, database).Delete(&model.DatabaseUserGrant{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if len(grants) != 0 {
|
||||
return tx.Create(&grants).Error
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (u *DatabaseUserGrantRepo) Delete(opts ...DBOption) error {
|
||||
db := global.DB
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
return db.Delete(&model.DatabaseUserGrant{}).Error
|
||||
}
|
||||
|
||||
func (u *DatabaseUserGrantRepo) Update(vars map[string]interface{}, opts ...DBOption) error {
|
||||
db := global.DB.Model(&model.DatabaseUserGrant{})
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
return db.Updates(vars).Error
|
||||
}
|
||||
|
||||
func (u *DatabaseUserGrantRepo) WithByDatabase(database string) DBOption {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
return db.Where("database = ?", database)
|
||||
}
|
||||
}
|
||||
|
||||
func (u *DatabaseUserGrantRepo) WithByDBName(dbName string) DBOption {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
return db.Where("db_name = ?", dbName)
|
||||
}
|
||||
}
|
||||
|
||||
func (u *DatabaseUserGrantRepo) WithByDBNames(dbNames []string) DBOption {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
return db.Where("db_name IN ?", dbNames)
|
||||
}
|
||||
}
|
||||
|
||||
func (u *DatabaseUserGrantRepo) WithByUser(username, host string) DBOption {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
return db.Where("username = ? AND host = ?", username, host)
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ type IWebsiteRepo interface {
|
||||
WithParentID(websiteID uint) DBOption
|
||||
WithType(websiteType string) DBOption
|
||||
WithDBType(dbType string) DBOption
|
||||
WithDBTypes(dbTypes []string) DBOption
|
||||
WithDBID(dbID uint) DBOption
|
||||
|
||||
Page(page, size int, opts ...DBOption) (int64, []model.Website, error)
|
||||
@@ -132,6 +133,12 @@ func (w *WebsiteRepo) WithDBType(dbType string) DBOption {
|
||||
}
|
||||
}
|
||||
|
||||
func (w *WebsiteRepo) WithDBTypes(dbTypes []string) DBOption {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
return db.Where("db_type IN ?", dbTypes)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *WebsiteRepo) WithDBID(dbID uint) DBOption {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
return db.Where("db_id = ?", dbID)
|
||||
|
||||
@@ -529,7 +529,7 @@ func ensureAppMysqlDBUser(database model.Database, dbConfig dto.AppDatabase) err
|
||||
userExists := false
|
||||
passwordValid := false
|
||||
for _, user := range users {
|
||||
if user.Username != dbConfig.DbUser || user.Host != host {
|
||||
if user.Username != dbConfig.DbUser || user.Host != host || user.IsDelete {
|
||||
continue
|
||||
}
|
||||
userExists = true
|
||||
@@ -566,19 +566,20 @@ func deleteLink(del dto.DelAppLink) error {
|
||||
for _, re := range resources {
|
||||
switch re.Key {
|
||||
case constant.AppMysql, constant.AppMariaDB:
|
||||
mysqlService := NewIMysqlService()
|
||||
database, _ := mysqlRepo.Get(repo.WithByID(re.ResourceId))
|
||||
if reflect.DeepEqual(database, model.DatabaseMysql{}) {
|
||||
continue
|
||||
}
|
||||
if err := mysqlService.Delete(del.Ctx, dto.MysqlDBDelete{
|
||||
if err := deleteMysqlDatabaseForResourceOwner(del.Ctx, dto.MysqlDBDelete{
|
||||
ID: database.ID,
|
||||
ForceDelete: del.ForceDelete,
|
||||
DeleteBackup: true,
|
||||
Type: re.Key,
|
||||
Database: database.MysqlName,
|
||||
}); err != nil && !del.ForceDelete {
|
||||
return err
|
||||
}, dto.DBResource{Type: constant.TypeApp, Name: install.Name}); err != nil {
|
||||
if isMysqlDatabaseResourceInUseError(err) || !del.ForceDelete {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case constant.AppPostgresql:
|
||||
pgsqlService := NewIPostgresqlService()
|
||||
|
||||
@@ -280,7 +280,7 @@ func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverF
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newDB, err := reCreateDB(db.ID, database, backupEnvMap)
|
||||
newDB, err := reCreateDB(db.ID, database, backupEnvMap, install.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -408,10 +408,16 @@ func doAppBackup(install *model.AppInstall, parentTask *task.Task, backupDir, fi
|
||||
return nil
|
||||
}
|
||||
|
||||
func reCreateDB(dbID uint, database model.Database, envMap map[string]interface{}) (*model.DatabaseMysql, error) {
|
||||
func reCreateDB(dbID uint, database model.Database, envMap map[string]interface{}, appInstallName string) (*model.DatabaseMysql, error) {
|
||||
mysqlService := NewIMysqlService()
|
||||
ctx := context.Background()
|
||||
_ = mysqlService.Delete(ctx, dto.MysqlDBDelete{ID: dbID, Database: database.Name, Type: database.Type, DeleteBackup: false, ForceDelete: true})
|
||||
if err := deleteMysqlDatabaseForResourceOwner(
|
||||
ctx,
|
||||
dto.MysqlDBDelete{ID: dbID, Database: database.Name, Type: database.Type, DeleteBackup: false, ForceDelete: true},
|
||||
dto.DBResource{Type: constant.TypeApp, Name: appInstallName},
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dbInfo := getDBCreateInfoFromEnv(envMap, "utf8mb4")
|
||||
createDB, err := mysqlService.Create(context.Background(), dto.MysqlDBCreate{
|
||||
@@ -442,7 +448,7 @@ func ensureMysqlDBUser(mysqlService IMysqlService, database model.Database, dbIn
|
||||
var oldUser dto.MysqlUser
|
||||
exists := false
|
||||
for _, user := range users {
|
||||
if user.Username == dbInfo.User && user.Host == host {
|
||||
if user.Username == dbInfo.User && user.Host == host && !user.IsDelete {
|
||||
oldUser = user
|
||||
exists = true
|
||||
break
|
||||
|
||||
+382
-124
@@ -7,6 +7,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -28,6 +29,7 @@ import (
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/jinzhu/copier"
|
||||
"github.com/pkg/errors"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type MysqlService struct{}
|
||||
@@ -50,6 +52,7 @@ type IMysqlService interface {
|
||||
CreateUser(req dto.MysqlUserCreate) error
|
||||
UpdateUser(req dto.MysqlUserUpdate) error
|
||||
ChangeUserPassword(req dto.MysqlUserPassword) error
|
||||
SaveUserPassword(req dto.MysqlUserPassword) error
|
||||
DeleteUser(req dto.MysqlUserDelete) error
|
||||
GrantUser(req dto.MysqlGrantCreate) error
|
||||
RevokeGrant(req dto.MysqlGrantDelete) error
|
||||
@@ -148,68 +151,118 @@ func saveDatabaseUserCredentials(dbType, database, username, permission, passwor
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadDatabaseUserMetaMap(dbType, database string) (map[string]model.DatabaseUser, error) {
|
||||
userMetas, err := databaseUserRepo.List(repo.WithByType(normalizeDatabaseUserType(dbType)), databaseUserRepo.WithByDatabase(database))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
metaMap := make(map[string]model.DatabaseUser, len(userMetas))
|
||||
for _, item := range userMetas {
|
||||
metaMap[databaseUserKey(item.Username, item.Host)] = item
|
||||
}
|
||||
return metaMap, nil
|
||||
}
|
||||
|
||||
type mysqlPasswordAppTarget struct {
|
||||
type mysqlUserAppTarget struct {
|
||||
Key string
|
||||
Name string
|
||||
}
|
||||
|
||||
func loadMysqlPasswordAppTargets(dbType, database, username, host string, grants []client.GrantInfo) ([]mysqlPasswordAppTarget, error) {
|
||||
targets := make([]mysqlPasswordAppTarget, 0)
|
||||
for _, grant := range grants {
|
||||
if grant.Username != username || grant.Host != host {
|
||||
func loadMysqlUserAppTargets(dbType, database, username, host string, dbNames ...string) ([]mysqlUserAppTarget, error) {
|
||||
if host != "%" {
|
||||
return nil, nil
|
||||
}
|
||||
dbNameSet := make(map[string]struct{}, len(dbNames))
|
||||
for _, dbName := range dbNames {
|
||||
dbNameSet[dbName] = struct{}{}
|
||||
}
|
||||
dbItems, err := mysqlRepo.List(mysqlRepo.WithByMysqlName(database))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
localResourceIDs := make([]uint, 0, len(dbItems))
|
||||
remoteResourceIDs := make([]uint, 0, len(dbItems))
|
||||
for _, dbItem := range dbItems {
|
||||
if len(dbNameSet) != 0 {
|
||||
if _, ok := dbNameSet[dbItem.Name]; !ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if dbItem.ID == 0 {
|
||||
continue
|
||||
}
|
||||
dbItem, err := mysqlRepo.Get(mysqlRepo.WithByMysqlName(database), repo.WithByName(grant.Database))
|
||||
if err != nil || dbItem.ID == 0 {
|
||||
continue
|
||||
}
|
||||
var appRess []model.AppInstallResource
|
||||
if dbItem.From == "local" {
|
||||
app, err := appInstallRepo.LoadBaseInfo(dbType, database)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
appRess, err = appInstallResourceRepo.GetBy(appInstallResourceRepo.WithLinkId(app.ID), appInstallResourceRepo.WithResourceId(dbItem.ID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
localResourceIDs = append(localResourceIDs, dbItem.ID)
|
||||
} else {
|
||||
appRess, err = appInstallResourceRepo.GetBy(appInstallResourceRepo.WithResourceId(dbItem.ID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
remoteResourceIDs = append(remoteResourceIDs, dbItem.ID)
|
||||
}
|
||||
for _, appRes := range appRess {
|
||||
appInstall, err := appInstallRepo.GetFirst(repo.WithByID(appRes.AppInstallId))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
appModel, err := appRepo.GetFirst(repo.WithByID(appInstall.AppId))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := os.ReadFile(appInstall.GetEnvPath()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
targets = append(targets, mysqlPasswordAppTarget{Key: appModel.Key, Name: appInstall.Name})
|
||||
}
|
||||
|
||||
appResources := make([]model.AppInstallResource, 0)
|
||||
if len(localResourceIDs) != 0 {
|
||||
app, err := appInstallRepo.LoadBaseInfo(dbType, database)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resources, err := appInstallResourceRepo.GetBy(
|
||||
appInstallResourceRepo.WithLinkId(app.ID),
|
||||
appInstallResourceRepo.WithResourceIds(localResourceIDs),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
appResources = append(appResources, resources...)
|
||||
}
|
||||
if len(remoteResourceIDs) != 0 {
|
||||
resources, err := appInstallResourceRepo.GetBy(
|
||||
appInstallResourceRepo.WithResourceIds(remoteResourceIDs),
|
||||
appRepo.WithKey(dbType),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
appResources = append(appResources, resources...)
|
||||
}
|
||||
|
||||
appInstallIDs := make([]uint, 0, len(appResources))
|
||||
appInstallIDSet := make(map[uint]struct{}, len(appResources))
|
||||
for _, appResource := range appResources {
|
||||
if appResource.AppInstallId == 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := appInstallIDSet[appResource.AppInstallId]; ok {
|
||||
continue
|
||||
}
|
||||
appInstallIDSet[appResource.AppInstallId] = struct{}{}
|
||||
appInstallIDs = append(appInstallIDs, appResource.AppInstallId)
|
||||
}
|
||||
if len(appInstallIDs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
appInstalls, err := appInstallRepo.ListBy(context.Background(), repo.WithByIDs(appInstallIDs))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
targets := make([]mysqlUserAppTarget, 0, len(appInstalls))
|
||||
for _, appInstall := range appInstalls {
|
||||
var envMap map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(appInstall.Env), &envMap); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
appUsername, ok := envMap["PANEL_DB_USER"].(string)
|
||||
if !ok || appUsername != username {
|
||||
continue
|
||||
}
|
||||
targets = append(targets, mysqlUserAppTarget{Key: appInstall.App.Key, Name: appInstall.Name})
|
||||
}
|
||||
return targets, nil
|
||||
}
|
||||
|
||||
func updateMysqlPasswordAppTargets(targets []mysqlPasswordAppTarget, password string) error {
|
||||
func checkMysqlUserAppUsage(dbType, database, username, host string, dbNames ...string) error {
|
||||
targets, err := loadMysqlUserAppTargets(dbType, database, username, host, dbNames...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(targets) == 0 {
|
||||
return nil
|
||||
}
|
||||
appNames := make([]string, 0, len(targets))
|
||||
for _, target := range targets {
|
||||
appNames = append(appNames, target.Name)
|
||||
}
|
||||
sort.Strings(appNames)
|
||||
return buserr.WithDetail("ErrMysqlUserUsedByApps", strings.Join(appNames, ", "), nil)
|
||||
}
|
||||
|
||||
func updateMysqlPasswordAppTargets(targets []mysqlUserAppTarget, password string) error {
|
||||
for _, target := range targets {
|
||||
global.LOG.Infof("start to update mysql password used by app %s-%s", target.Key, target.Name)
|
||||
if err := updateInstallInfoInDB(target.Key, target.Name, "user-password", password); err != nil {
|
||||
@@ -238,7 +291,7 @@ func syncDatabaseUserMetadata(dbType, database string, users []client.UserInfo)
|
||||
userMap[key] = struct{}{}
|
||||
if meta, ok := metaMap[key]; ok {
|
||||
if meta.IsDelete {
|
||||
if err := databaseUserRepo.Update(map[string]interface{}{"is_delete": false}, repo.WithByType(dbType), databaseUserRepo.WithByDatabase(database), databaseUserRepo.WithByUser(item.Username, item.Host)); err != nil {
|
||||
if err := databaseUserRepo.Update(map[string]interface{}{"is_delete": false, "password": ""}, repo.WithByType(dbType), databaseUserRepo.WithByDatabase(database), databaseUserRepo.WithByUser(item.Username, item.Host)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -263,13 +316,60 @@ func syncDatabaseUserMetadata(dbType, database string, users []client.UserInfo)
|
||||
if item.IsDelete {
|
||||
continue
|
||||
}
|
||||
if err := databaseUserRepo.Update(map[string]interface{}{"is_delete": true}, repo.WithByType(dbType), databaseUserRepo.WithByDatabase(database), databaseUserRepo.WithByUser(item.Username, item.Host)); err != nil {
|
||||
if err := databaseUserRepo.Update(map[string]interface{}{"is_delete": true, "password": ""}, repo.WithByType(dbType), databaseUserRepo.WithByDatabase(database), databaseUserRepo.WithByUser(item.Username, item.Host)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func saveDatabaseUserGrant(dbType, database, dbName, username, host string) error {
|
||||
dbType = normalizeDatabaseUserType(dbType)
|
||||
grant, err := databaseUserGrantRepo.Get(
|
||||
repo.WithByType(dbType),
|
||||
databaseUserGrantRepo.WithByDatabase(database),
|
||||
databaseUserGrantRepo.WithByDBName(dbName),
|
||||
databaseUserGrantRepo.WithByUser(username, host),
|
||||
)
|
||||
if err == nil && grant.ID != 0 {
|
||||
return nil
|
||||
}
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
return databaseUserGrantRepo.Save(&model.DatabaseUserGrant{
|
||||
Type: dbType,
|
||||
Database: database,
|
||||
DBName: dbName,
|
||||
Username: username,
|
||||
Host: host,
|
||||
})
|
||||
}
|
||||
|
||||
func syncDatabaseUserGrants(dbType, database string, grants []client.GrantInfo) error {
|
||||
dbType = normalizeDatabaseUserType(dbType)
|
||||
items := make([]model.DatabaseUserGrant, 0, len(grants))
|
||||
grantMap := make(map[string]struct{}, len(grants))
|
||||
for _, item := range grants {
|
||||
if isMysqlSystemUser(item.Username) || item.Database == "*" {
|
||||
continue
|
||||
}
|
||||
key := item.Database + "\x00" + item.Username + "\x00" + item.Host
|
||||
if _, ok := grantMap[key]; ok {
|
||||
continue
|
||||
}
|
||||
grantMap[key] = struct{}{}
|
||||
items = append(items, model.DatabaseUserGrant{
|
||||
Type: dbType,
|
||||
Database: database,
|
||||
DBName: item.Database,
|
||||
Username: item.Username,
|
||||
Host: item.Host,
|
||||
})
|
||||
}
|
||||
return databaseUserGrantRepo.Replace(dbType, database, items)
|
||||
}
|
||||
|
||||
func (u *MysqlService) SearchWithPage(search dto.MysqlDBSearch) (int64, interface{}, error) {
|
||||
total, mysqls, err := mysqlRepo.Page(search.Page, search.PageSize,
|
||||
mysqlRepo.WithByMysqlName(search.Database),
|
||||
@@ -368,6 +468,11 @@ func (u *MysqlService) Create(ctx context.Context, req dto.MysqlDBCreate) (*mode
|
||||
if err := saveDatabaseUserCredentials(dbType, req.Database, req.Username, req.Permission, req.Password, req.Description); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, host := range splitMysqlHosts(req.Permission) {
|
||||
if err := saveDatabaseUserGrant(dbType, req.Database, req.Name, req.Username, host); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
global.LOG.Infof("create database %s successful!", req.Name)
|
||||
@@ -382,50 +487,38 @@ func (u *MysqlService) ListUsers(req dto.MysqlUserSearch) ([]dto.MysqlUser, erro
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cli, _, err := LoadMysqlClientByFrom(req.Database)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cli.Close()
|
||||
users, err := cli.ListUsers(300)
|
||||
users, err := databaseUserRepo.List(repo.WithByType(dbType), databaseUserRepo.WithByDatabase(req.Database))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res := make([]dto.MysqlUser, 0, len(users))
|
||||
metaMap, err := loadDatabaseUserMetaMap(dbType, req.Database)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, user := range users {
|
||||
if isMysqlSystemUser(user.Username) {
|
||||
continue
|
||||
}
|
||||
item := dto.MysqlUser{Username: user.Username, Host: user.Host}
|
||||
if meta, ok := metaMap[databaseUserKey(user.Username, user.Host)]; ok {
|
||||
item.Password = meta.Password
|
||||
item.Description = meta.Description
|
||||
}
|
||||
res = append(res, item)
|
||||
res = append(res, dto.MysqlUser{
|
||||
Username: user.Username,
|
||||
Host: user.Host,
|
||||
Password: user.Password,
|
||||
Description: user.Description,
|
||||
IsDelete: user.IsDelete,
|
||||
})
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (u *MysqlService) ListGrants(req dto.MysqlUserSearch) ([]dto.MysqlGrant, error) {
|
||||
cli, _, err := LoadMysqlClientByFrom(req.Database)
|
||||
dbType, err := resolveDatabaseUserType(req.Database)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cli.Close()
|
||||
grants, err := cli.ListGrants(300)
|
||||
grants, err := databaseUserGrantRepo.List(repo.WithByType(dbType), databaseUserGrantRepo.WithByDatabase(req.Database))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res := make([]dto.MysqlGrant, 0, len(grants))
|
||||
for _, grant := range grants {
|
||||
if isMysqlSystemUser(grant.Username) {
|
||||
continue
|
||||
}
|
||||
res = append(res, dto.MysqlGrant{Database: grant.Database, Username: grant.Username, Host: grant.Host})
|
||||
res = append(res, dto.MysqlGrant{Database: grant.DBName, Username: grant.Username, Host: grant.Host})
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
@@ -433,11 +526,16 @@ func (u *MysqlService) ListGrants(req dto.MysqlUserSearch) ([]dto.MysqlGrant, er
|
||||
func (u *MysqlService) ListGrantSummary(req dto.MysqlGrantSummarySearch) (map[string][]dto.MysqlUser, error) {
|
||||
res := make(map[string][]dto.MysqlUser, len(req.DBs))
|
||||
dbMap := make(map[string]struct{}, len(req.DBs))
|
||||
dbNames := make([]string, 0, len(req.DBs))
|
||||
for _, item := range req.DBs {
|
||||
if item == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := dbMap[item]; ok {
|
||||
continue
|
||||
}
|
||||
dbMap[item] = struct{}{}
|
||||
dbNames = append(dbNames, item)
|
||||
res[item] = []dto.MysqlUser{}
|
||||
}
|
||||
if len(dbMap) == 0 {
|
||||
@@ -447,35 +545,47 @@ func (u *MysqlService) ListGrantSummary(req dto.MysqlGrantSummarySearch) (map[st
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cli, _, err := LoadMysqlClientByFrom(req.Database)
|
||||
grants, err := databaseUserGrantRepo.List(
|
||||
repo.WithByType(dbType),
|
||||
databaseUserGrantRepo.WithByDatabase(req.Database),
|
||||
databaseUserGrantRepo.WithByDBNames(dbNames),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cli.Close()
|
||||
grants, err := cli.ListGrants(300)
|
||||
userList := make([][2]string, 0, len(grants))
|
||||
userSet := make(map[string]struct{}, len(grants))
|
||||
for _, grant := range grants {
|
||||
key := databaseUserKey(grant.Username, grant.Host)
|
||||
if _, ok := userSet[key]; ok {
|
||||
continue
|
||||
}
|
||||
userSet[key] = struct{}{}
|
||||
userList = append(userList, [2]string{grant.Username, grant.Host})
|
||||
}
|
||||
userMetas, err := databaseUserRepo.List(
|
||||
repo.WithByType(dbType),
|
||||
databaseUserRepo.WithByDatabase(req.Database),
|
||||
databaseUserRepo.WithByUserList(userList),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
metaMap, err := loadDatabaseUserMetaMap(dbType, req.Database)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
metaMap := make(map[string]model.DatabaseUser, len(userMetas))
|
||||
for _, item := range userMetas {
|
||||
metaMap[databaseUserKey(item.Username, item.Host)] = item
|
||||
}
|
||||
|
||||
for _, grant := range grants {
|
||||
if isMysqlSystemUser(grant.Username) {
|
||||
if _, ok := dbMap[grant.DBName]; !ok {
|
||||
continue
|
||||
}
|
||||
if _, ok := dbMap[grant.Database]; !ok {
|
||||
meta, ok := metaMap[databaseUserKey(grant.Username, grant.Host)]
|
||||
if !ok || meta.IsDelete {
|
||||
continue
|
||||
}
|
||||
item := dto.MysqlUser{Username: grant.Username, Host: grant.Host}
|
||||
if meta, ok := metaMap[databaseUserKey(grant.Username, grant.Host)]; ok {
|
||||
item.Password = meta.Password
|
||||
item.Description = meta.Description
|
||||
}
|
||||
res[grant.Database] = append(res[grant.Database], item)
|
||||
item := dto.MysqlUser{Username: grant.Username, Host: grant.Host, Description: meta.Description}
|
||||
res[grant.DBName] = append(res[grant.DBName], item)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
@@ -518,6 +628,9 @@ func (u *MysqlService) UpdateUser(req dto.MysqlUserUpdate) error {
|
||||
if targetUser.ID != 0 {
|
||||
return buserr.New("ErrRecordExist")
|
||||
}
|
||||
if err := checkMysqlUserAppUsage(dbType, req.Database, req.Username, req.Host); err != nil {
|
||||
return err
|
||||
}
|
||||
cli, _, err := LoadMysqlClientByFrom(req.Database)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -543,6 +656,14 @@ func (u *MysqlService) UpdateUser(req dto.MysqlUserUpdate) error {
|
||||
}
|
||||
return err
|
||||
}
|
||||
if err := databaseUserGrantRepo.Update(
|
||||
map[string]interface{}{"host": req.NewHost},
|
||||
repo.WithByType(dbType),
|
||||
databaseUserGrantRepo.WithByDatabase(req.Database),
|
||||
databaseUserGrantRepo.WithByUser(req.Username, req.Host),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
user, err := databaseUserRepo.Get(repo.WithByType(dbType), databaseUserRepo.WithByDatabase(req.Database), databaseUserRepo.WithByUser(req.Username, req.Host))
|
||||
@@ -579,9 +700,18 @@ func (u *MysqlService) ChangeUserPassword(req dto.MysqlUserPassword) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
appTargets, err := loadMysqlPasswordAppTargets(dbType, req.Database, req.Username, req.Host, grants)
|
||||
if err != nil {
|
||||
return err
|
||||
grantDBs := make([]string, 0)
|
||||
for _, grant := range grants {
|
||||
if grant.Username == req.Username && grant.Host == req.Host {
|
||||
grantDBs = append(grantDBs, grant.Database)
|
||||
}
|
||||
}
|
||||
var appTargets []mysqlUserAppTarget
|
||||
if len(grantDBs) != 0 {
|
||||
appTargets, err = loadMysqlUserAppTargets(dbType, req.Database, req.Username, req.Host, grantDBs...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := cli.ChangePassword(client.PasswordChangeInfo{
|
||||
Username: req.Username,
|
||||
@@ -609,6 +739,28 @@ func (u *MysqlService) ChangeUserPassword(req dto.MysqlUserPassword) error {
|
||||
return updateMysqlPasswordAppTargets(appTargets, req.Password)
|
||||
}
|
||||
|
||||
func (u *MysqlService) SaveUserPassword(req dto.MysqlUserPassword) error {
|
||||
if cmd.CheckIllegal(req.Username, req.Host, req.Password) {
|
||||
return buserr.New("ErrCmdIllegal")
|
||||
}
|
||||
if err := checkMysqlNormalUser(req.Username); err != nil {
|
||||
return err
|
||||
}
|
||||
dbType, err := resolveDatabaseUserType(req.Database)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user, err := databaseUserRepo.Get(repo.WithByType(dbType), databaseUserRepo.WithByDatabase(req.Database), databaseUserRepo.WithByUser(req.Username, req.Host))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if user.IsDelete {
|
||||
return errors.New("cannot save password for a deleted mysql user")
|
||||
}
|
||||
user.Password = req.Password
|
||||
return databaseUserRepo.Save(&user)
|
||||
}
|
||||
|
||||
func (u *MysqlService) DeleteUser(req dto.MysqlUserDelete) error {
|
||||
if cmd.CheckIllegal(req.Username, req.Host) {
|
||||
return buserr.New("ErrCmdIllegal")
|
||||
@@ -620,16 +772,27 @@ func (u *MysqlService) DeleteUser(req dto.MysqlUserDelete) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cli, version, err := LoadMysqlClientByFrom(req.Database)
|
||||
if err != nil {
|
||||
if err := checkMysqlUserAppUsage(dbType, req.Database, req.Username, req.Host); err != nil {
|
||||
return err
|
||||
}
|
||||
defer cli.Close()
|
||||
if err := cli.DeleteUser(client.UserInfo{Username: req.Username, Host: req.Host}, version, 300); err != nil {
|
||||
user, err := databaseUserRepo.Get(repo.WithByType(dbType), databaseUserRepo.WithByDatabase(req.Database), databaseUserRepo.WithByUser(req.Username, req.Host))
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
_ = databaseUserRepo.Delete(repo.WithByType(dbType), databaseUserRepo.WithByDatabase(req.Database), databaseUserRepo.WithByUser(req.Username, req.Host))
|
||||
return nil
|
||||
if err != nil || !user.IsDelete {
|
||||
cli, version, err := LoadMysqlClientByFrom(req.Database)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cli.Close()
|
||||
if err := cli.DeleteUser(client.UserInfo{Username: req.Username, Host: req.Host}, version, 300); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := databaseUserGrantRepo.Delete(repo.WithByType(dbType), databaseUserGrantRepo.WithByDatabase(req.Database), databaseUserGrantRepo.WithByUser(req.Username, req.Host)); err != nil {
|
||||
return err
|
||||
}
|
||||
return databaseUserRepo.Delete(repo.WithByType(dbType), databaseUserRepo.WithByDatabase(req.Database), databaseUserRepo.WithByUser(req.Username, req.Host))
|
||||
}
|
||||
|
||||
func (u *MysqlService) GrantUser(req dto.MysqlGrantCreate) error {
|
||||
@@ -642,6 +805,10 @@ func (u *MysqlService) GrantUser(req dto.MysqlGrantCreate) error {
|
||||
if err := checkMysqlNormalUser(req.Username); err != nil {
|
||||
return err
|
||||
}
|
||||
dbType, err := resolveDatabaseUserType(req.Database)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cli, _, err := LoadMysqlClientByFrom(req.Database)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -650,7 +817,7 @@ func (u *MysqlService) GrantUser(req dto.MysqlGrantCreate) error {
|
||||
if err := cli.GrantUser(client.GrantInfo{Database: req.DB, Username: req.Username, Host: req.Host}, 300); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return saveDatabaseUserGrant(dbType, req.Database, req.DB, req.Username, req.Host)
|
||||
}
|
||||
|
||||
func (u *MysqlService) RevokeGrant(req dto.MysqlGrantDelete) error {
|
||||
@@ -663,6 +830,13 @@ func (u *MysqlService) RevokeGrant(req dto.MysqlGrantDelete) error {
|
||||
if err := checkMysqlNormalUser(req.Username); err != nil {
|
||||
return err
|
||||
}
|
||||
dbType, err := resolveDatabaseUserType(req.Database)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := checkMysqlUserAppUsage(dbType, req.Database, req.Username, req.Host, req.DB); err != nil {
|
||||
return err
|
||||
}
|
||||
cli, _, err := LoadMysqlClientByFrom(req.Database)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -671,7 +845,12 @@ func (u *MysqlService) RevokeGrant(req dto.MysqlGrantDelete) error {
|
||||
if err := cli.RevokeGrant(client.GrantInfo{Database: req.DB, Username: req.Username, Host: req.Host}, 300); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return databaseUserGrantRepo.Delete(
|
||||
repo.WithByType(dbType),
|
||||
databaseUserGrantRepo.WithByDatabase(req.Database),
|
||||
databaseUserGrantRepo.WithByDBName(req.DB),
|
||||
databaseUserGrantRepo.WithByUser(req.Username, req.Host),
|
||||
)
|
||||
}
|
||||
|
||||
func (u *MysqlService) LoadFromRemote(req dto.MysqlLoadDB) error {
|
||||
@@ -683,6 +862,7 @@ func (u *MysqlService) LoadFromRemote(req dto.MysqlLoadDB) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
databases, err := mysqlRepo.List(mysqlRepo.WithByMysqlName(req.Database))
|
||||
if err != nil {
|
||||
@@ -696,9 +876,16 @@ func (u *MysqlService) LoadFromRemote(req dto.MysqlLoadDB) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
grants, err := client.ListGrants(300)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := syncDatabaseUserMetadata(dbType, req.Database, users); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := syncDatabaseUserGrants(dbType, req.Database, grants); err != nil {
|
||||
return err
|
||||
}
|
||||
deleteList := databases
|
||||
for _, data := range datas {
|
||||
hasOld := false
|
||||
@@ -735,22 +922,28 @@ func (u *MysqlService) UpdateDescription(req dto.UpdateDescription) error {
|
||||
return mysqlRepo.Update(req.ID, map[string]interface{}{"description": req.Description})
|
||||
}
|
||||
|
||||
func (u *MysqlService) DeleteCheck(req dto.MysqlDBDeleteCheck) ([]dto.DBResource, error) {
|
||||
func loadMysqlDeleteTarget(id uint) (model.DatabaseMysql, string, error) {
|
||||
db, err := mysqlRepo.Get(repo.WithByID(id))
|
||||
if err != nil {
|
||||
return db, "", err
|
||||
}
|
||||
dbType, err := resolveDatabaseUserType(db.MysqlName)
|
||||
if err != nil {
|
||||
return db, "", err
|
||||
}
|
||||
return db, dbType, nil
|
||||
}
|
||||
|
||||
func (u *MysqlService) deleteCheck(db model.DatabaseMysql, dbType string) ([]dto.DBResource, error) {
|
||||
var res []dto.DBResource
|
||||
db, err := mysqlRepo.Get(repo.WithByID(req.ID))
|
||||
websites, err := websiteRepo.GetBy(
|
||||
websiteRepo.WithDBTypes([]string{constant.AppMysql, constant.AppMariaDB, constant.AppMysqlCluster}),
|
||||
websiteRepo.WithDBID(db.ID),
|
||||
)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
website, _ := websiteRepo.GetFirst(websiteRepo.WithDBType(constant.AppMysql), websiteRepo.WithDBID(req.ID))
|
||||
if website.ID != 0 {
|
||||
res = append(res, dto.DBResource{
|
||||
Type: constant.TypeWebsite,
|
||||
Name: website.PrimaryDomain,
|
||||
})
|
||||
}
|
||||
website, _ = websiteRepo.GetFirst(websiteRepo.WithDBType(constant.AppMysqlCluster), websiteRepo.WithDBID(req.ID))
|
||||
if website.ID != 0 {
|
||||
for _, website := range websites {
|
||||
res = append(res, dto.DBResource{
|
||||
Type: constant.TypeWebsite,
|
||||
Name: website.PrimaryDomain,
|
||||
@@ -758,13 +951,22 @@ func (u *MysqlService) DeleteCheck(req dto.MysqlDBDeleteCheck) ([]dto.DBResource
|
||||
}
|
||||
|
||||
if db.From == "local" {
|
||||
app, err := appInstallRepo.LoadBaseInfo(req.Type, req.Database)
|
||||
app, err := appInstallRepo.LoadBaseInfo(dbType, db.MysqlName)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
apps, err := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithLinkId(app.ID), appInstallResourceRepo.WithResourceId(db.ID))
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
apps, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithLinkId(app.ID), appInstallResourceRepo.WithResourceId(db.ID))
|
||||
for _, app := range apps {
|
||||
appInstall, _ := appInstallRepo.GetFirst(repo.WithByID(app.AppInstallId))
|
||||
appInstall, err := appInstallRepo.GetFirst(repo.WithByID(app.AppInstallId))
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
continue
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
if appInstall.ID != 0 {
|
||||
res = append(res, dto.DBResource{
|
||||
Type: constant.TypeApp,
|
||||
@@ -773,9 +975,18 @@ func (u *MysqlService) DeleteCheck(req dto.MysqlDBDeleteCheck) ([]dto.DBResource
|
||||
}
|
||||
}
|
||||
} else {
|
||||
apps, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithResourceId(db.ID), appRepo.WithKey(req.Type))
|
||||
apps, err := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithResourceId(db.ID), appRepo.WithKey(dbType))
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
for _, app := range apps {
|
||||
appInstall, _ := appInstallRepo.GetFirst(repo.WithByID(app.AppInstallId))
|
||||
appInstall, err := appInstallRepo.GetFirst(repo.WithByID(app.AppInstallId))
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
continue
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
if appInstall.ID != 0 {
|
||||
res = append(res, dto.DBResource{
|
||||
Type: constant.TypeApp,
|
||||
@@ -788,12 +999,50 @@ func (u *MysqlService) DeleteCheck(req dto.MysqlDBDeleteCheck) ([]dto.DBResource
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (u *MysqlService) DeleteCheck(req dto.MysqlDBDeleteCheck) ([]dto.DBResource, error) {
|
||||
db, dbType, err := loadMysqlDeleteTarget(req.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return u.deleteCheck(db, dbType)
|
||||
}
|
||||
|
||||
func (u *MysqlService) Delete(ctx context.Context, req dto.MysqlDBDelete) error {
|
||||
db, err := mysqlRepo.Get(repo.WithByID(req.ID))
|
||||
if err != nil && !req.ForceDelete {
|
||||
return u.delete(ctx, req, nil)
|
||||
}
|
||||
|
||||
func (u *MysqlService) delete(ctx context.Context, req dto.MysqlDBDelete, exclusions []dto.DBResource) error {
|
||||
db, dbType, err := loadMysqlDeleteTarget(req.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cli, version, err := LoadMysqlClientByFrom(req.Database)
|
||||
resources, err := u.deleteCheck(db, dbType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(exclusions) != 0 {
|
||||
exclusionMap := make(map[string]struct{}, len(exclusions))
|
||||
for _, exclusion := range exclusions {
|
||||
exclusionMap[exclusion.Type+"\x00"+exclusion.Name] = struct{}{}
|
||||
}
|
||||
filtered := resources[:0]
|
||||
for _, resource := range resources {
|
||||
if _, ok := exclusionMap[resource.Type+"\x00"+resource.Name]; ok {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, resource)
|
||||
}
|
||||
resources = filtered
|
||||
}
|
||||
if len(resources) != 0 {
|
||||
names := make([]string, 0, len(resources))
|
||||
for _, resource := range resources {
|
||||
names = append(names, resource.Name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
return buserr.WithDetail("ErrInUsed", strings.Join(names, ", "), nil)
|
||||
}
|
||||
cli, version, err := LoadMysqlClientByFrom(db.MysqlName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -807,22 +1056,31 @@ func (u *MysqlService) Delete(ctx context.Context, req dto.MysqlDBDelete) error
|
||||
}
|
||||
|
||||
if req.DeleteBackup {
|
||||
uploadDir := filepath.Join(global.Dir.DataDir, fmt.Sprintf("uploads/database/%s/%s/%s", req.Type, req.Database, db.Name))
|
||||
uploadDir := filepath.Join(global.Dir.DataDir, fmt.Sprintf("uploads/database/%s/%s/%s", dbType, db.MysqlName, db.Name))
|
||||
if _, err := os.Stat(uploadDir); err == nil {
|
||||
_ = os.RemoveAll(uploadDir)
|
||||
}
|
||||
backupDir := filepath.Join(global.Dir.LocalBackupDir, fmt.Sprintf("database/%s/%s/%s", req.Type, db.MysqlName, db.Name))
|
||||
backupDir := filepath.Join(global.Dir.LocalBackupDir, fmt.Sprintf("database/%s/%s/%s", dbType, db.MysqlName, db.Name))
|
||||
if _, err := os.Stat(backupDir); err == nil {
|
||||
_ = os.RemoveAll(backupDir)
|
||||
}
|
||||
_ = backupRepo.DeleteRecord(ctx, repo.WithByType(req.Type), repo.WithByName(req.Database), repo.WithByDetailName(db.Name))
|
||||
global.LOG.Infof("delete database %s-%s backups successful", req.Database, db.Name)
|
||||
_ = backupRepo.DeleteRecord(ctx, repo.WithByType(dbType), repo.WithByName(db.MysqlName), repo.WithByDetailName(db.Name))
|
||||
global.LOG.Infof("delete database %s-%s backups successful", db.MysqlName, db.Name)
|
||||
}
|
||||
|
||||
_ = mysqlRepo.Delete(ctx, repo.WithByID(db.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
func deleteMysqlDatabaseForResourceOwner(ctx context.Context, req dto.MysqlDBDelete, owner dto.DBResource) error {
|
||||
return (&MysqlService{}).delete(ctx, req, []dto.DBResource{owner})
|
||||
}
|
||||
|
||||
func isMysqlDatabaseResourceInUseError(err error) bool {
|
||||
businessErr, ok := err.(buserr.BusinessError)
|
||||
return ok && businessErr.Msg == "ErrInUsed"
|
||||
}
|
||||
|
||||
func (u *MysqlService) ChangePassword(req dto.ChangeDBInfo) error {
|
||||
if cmd.CheckIllegal(req.Value) {
|
||||
return buserr.New("ErrCmdIllegal")
|
||||
|
||||
@@ -19,11 +19,12 @@ var (
|
||||
agentAccountRepo = repo.NewIAgentAccountRepo()
|
||||
agentAccountModelRepo = repo.NewIAgentAccountModelRepo()
|
||||
|
||||
mysqlRepo = repo.NewIMysqlRepo()
|
||||
postgresqlRepo = repo.NewIPostgresqlRepo()
|
||||
mongodbRepo = repo.NewIMongodbRepo()
|
||||
databaseRepo = repo.NewIDatabaseRepo()
|
||||
databaseUserRepo = repo.NewIDatabaseUserRepo()
|
||||
mysqlRepo = repo.NewIMysqlRepo()
|
||||
postgresqlRepo = repo.NewIPostgresqlRepo()
|
||||
mongodbRepo = repo.NewIMongodbRepo()
|
||||
databaseRepo = repo.NewIDatabaseRepo()
|
||||
databaseUserRepo = repo.NewIDatabaseUserRepo()
|
||||
databaseUserGrantRepo = repo.NewIDatabaseUserGrantRepo()
|
||||
|
||||
imageRepoRepo = repo.NewIImageRepoRepo()
|
||||
composeRepo = repo.NewIComposeTemplateRepo()
|
||||
|
||||
@@ -715,11 +715,18 @@ func (w WebsiteService) DeleteWebsite(req request.WebsiteDelete) error {
|
||||
if mysqlDB.ID > 0 {
|
||||
deleteReq := dto.MysqlDBDelete{
|
||||
ID: mysqlDB.ID,
|
||||
Type: website.DbType,
|
||||
Database: mysqlDB.MysqlName,
|
||||
ForceDelete: req.ForceDelete,
|
||||
}
|
||||
if err = NewIMysqlService().Delete(context.TODO(), deleteReq); err != nil && !req.ForceDelete {
|
||||
return err
|
||||
if err = deleteMysqlDatabaseForResourceOwner(
|
||||
context.TODO(),
|
||||
deleteReq,
|
||||
dto.DBResource{Type: constant.TypeWebsite, Name: website.PrimaryDomain},
|
||||
); err != nil {
|
||||
if isMysqlDatabaseResourceInUseError(err) || !req.ForceDelete {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
case constant.AppPostgresql, constant.AppPostgres:
|
||||
|
||||
@@ -3120,6 +3120,17 @@
|
||||
"formatZH": "更新 mysql 数据库 [database] 用户 [username]@[host] 密码",
|
||||
"formatEN": "update mysql database [database] user [username]@[host] password"
|
||||
},
|
||||
"/databases/users/password/save": {
|
||||
"bodyKeys": [
|
||||
"database",
|
||||
"username",
|
||||
"host"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "补充 mysql 数据库 [database] 用户 [username]@[host] 密码",
|
||||
"formatEN": "save mysql database [database] user [username]@[host] password locally"
|
||||
},
|
||||
"/databases/users/update": {
|
||||
"bodyKeys": [
|
||||
"database",
|
||||
|
||||
@@ -110,6 +110,7 @@ ErrAppLimit: 'App install limit reached'
|
||||
ErrNotInstall: 'Application not installed'
|
||||
ErrPortInOtherApp: '{{ .port }} port is already occupied by application {{ .apps }}!'
|
||||
ErrDbUserNotValid: 'Database username/password mismatch'
|
||||
ErrMysqlUserUsedByApps: 'This database user is used by [{{ .detail }}]; its host cannot be changed, and it cannot be deleted or have its authorization revoked'
|
||||
ErrUpdateBuWebsite: 'App updated, but website config update failed'
|
||||
Err1PanelNetworkFailed: 'Default container network creation failed! {{ .detail }}'
|
||||
ErrFileParse: 'Failed to parse app docker-compose file'
|
||||
|
||||
@@ -110,6 +110,7 @@ ErrAppLimit: 'El número de aplicaciones instaladas ha superado el límite'
|
||||
ErrNotInstall: 'Aplicación no instalada'
|
||||
ErrPortInOtherApp: 'El puerto {{ .port }} ya está ocupado por la aplicación {{ .apps }}'
|
||||
ErrDbUserNotValid: 'Base de datos existente, usuario y contraseña no coinciden'
|
||||
ErrMysqlUserUsedByApps: 'Este usuario de base de datos está siendo utilizado por [{{ .detail }}]; no se puede cambiar su host, eliminarlo ni revocar su autorización'
|
||||
ErrUpdateBuWebsite: 'La aplicación se actualizó correctamente, pero falló la modificación del archivo de configuración del sitio web. revise la configuración'
|
||||
Err1PanelNetworkFailed: 'Fallo la creación de la red por defecto del contenedor {{ .detail }}'
|
||||
ErrFileParse: 'Fallo al analizar el archivo docker-compose de la aplicación'
|
||||
|
||||
@@ -110,6 +110,7 @@ ErrAppLimit: 'محدودیت نصب برنامه رسیده است'
|
||||
ErrNotInstall: 'برنامه نصب نشده است'
|
||||
ErrPortInOtherApp: 'پورت {{ .port }} قبلاً توسط برنامه {{ .apps }} اشغال شده است!'
|
||||
ErrDbUserNotValid: 'نام کاربری/رمز عبور پایگاه داده مطابقت ندارد'
|
||||
ErrMysqlUserUsedByApps: 'این کاربر پایگاه داده توسط [{{ .detail }}] استفاده میشود و میزبان آن قابل تغییر نیست؛ همچنین نمیتوان آن را حذف یا مجوزش را لغو کرد'
|
||||
ErrUpdateBuWebsite: 'برنامه بهروزرسانی شد، اما بهروزرسانی پیکربندی وبسایت ناموفق بود'
|
||||
Err1PanelNetworkFailed: 'ایجاد شبکه پیشفرض کانتینر ناموفق بود! {{ .detail }}'
|
||||
ErrFileParse: 'تجزیه فایل docker-compose برنامه ناموفق بود'
|
||||
|
||||
@@ -110,6 +110,7 @@ ErrAppLimit: 'インストールされているアプリケーションの数が
|
||||
ErrNotInstall: 'アプリケーションがインストールされていません'
|
||||
ErrPortInOtherApp: '{{ .port }} ポートは既にアプリケーション {{ .apps }} によって使用されています'
|
||||
ErrDbUserNotValid: '既存のデータベース、ユーザー名、およびパスワードが一致しません'
|
||||
ErrMysqlUserUsedByApps: 'このデータベースユーザーは【{{ .detail }}】で使用されているため、ホストの変更、削除、または権限の取り消しはできません'
|
||||
ErrUpdateBuWebsite: 'アプリケーションは正常に更新されましたが、Web サイト構成ファイルの変更に失敗しました。設定を確認してください'
|
||||
Err1PanelNetworkFailed: 'デフォルトのコンテナ ネットワークの作成に失敗しました。 {{ .detail }}'
|
||||
ErrFileParse: 'アプリケーションの docker-compose ファイルの解析に失敗しました'
|
||||
|
||||
@@ -110,6 +110,7 @@ ErrAppLimit: '설치된 애플리케이션 수가 한도를 초과했습니다'
|
||||
ErrNotInstall: '응용 프로그램이 설치되지 않았습니다'
|
||||
ErrPortInOtherApp: '{{ .port }} 포트는 이미 {{ .apps }} 애플리케이션에 의해 사용되고 있습니다'
|
||||
ErrDbUserNotValid: '기존 데이터베이스, 사용자 이름 및 비밀번호가 일치하지 않습니다'
|
||||
ErrMysqlUserUsedByApps: '이 데이터베이스 사용자는 [{{ .detail }}]에서 사용 중이므로 호스트 변경, 삭제 또는 권한 해제를 할 수 없습니다'
|
||||
ErrUpdateBuWebsite: '응용 프로그램이 성공적으로 업데이트되었지만, 웹사이트 구성 파일 수정에 실패했습니다. 구성을 확인하세요'
|
||||
Err1PanelNetworkFailed: '기본 컨테이너 네트워크 생성에 실패했습니다 {{ .세부 사항 }}'
|
||||
ErrFileParse: '응용 프로그램 docker-compose 파일 구문 분석에 실패했습니다'
|
||||
|
||||
@@ -95,6 +95,7 @@ ErrAppLimit: 'ຮອດຂີດຈຳກັດການຕິດຕັ້ງແ
|
||||
ErrNotInstall: 'ແອັບພລິເຄຊັນຍັງບໍ່ໄດ້ຕິດຕັ້ງ'
|
||||
ErrPortInOtherApp: 'ພອດ {{ .port }} ຖືກໃຊ້ງານແລ້ວໂດຍແອັບພລິເຄຊັນ {{ .apps }}!'
|
||||
ErrDbUserNotValid: 'ຊື່ຜູ້ໃຊ້ ຫຼື ລະຫັດຜ່ານຖານຂໍ້ມູນບໍ່ຖືກຕ້ອງ'
|
||||
ErrMysqlUserUsedByApps: 'ຜູ້ໃຊ້ຖານຂໍ້ມູນນີ້ກຳລັງຖືກໃຊ້ໂດຍ [{{ .detail }}] ແລະບໍ່ສາມາດປ່ຽນ Host, ລຶບ ຫຼືຖອນການອະນຸຍາດໄດ້'
|
||||
ErrUpdateBuWebsite: 'ອັບເດດແອັບແລ້ວ, ແຕ່ການອັບເດດການຕັ້ງຄ່າເວັບໄຊລົ້ມເຫຼວ'
|
||||
Err1PanelNetworkFailed: 'ການສ້າງເຄືອຂ່າຍຄອນເທນເນີເລີ່ມຕົ້ນລົ້ມເຫຼວ! {{ .detail }}'
|
||||
ErrFileParse: 'ການວິເຄາະໄຟລ໌ docker-compose ຂອງແອັບລົ້ມເຫຼວ'
|
||||
|
||||
@@ -110,6 +110,7 @@ ErrAppLimit: 'Bilangan aplikasi yang dipasang telah melebihi had'
|
||||
ErrNotInstall: 'Aplikasi tidak dipasang'
|
||||
ErrPortInOtherApp: 'Port {{ .port }} sudah diduduki oleh aplikasi {{ .apps }}'
|
||||
ErrDbUserNotValid: 'Pangkalan data sedia ada, nama pengguna dan kata laluan tidak sepadan'
|
||||
ErrMysqlUserUsedByApps: 'Pengguna pangkalan data ini sedang digunakan oleh [{{ .detail }}]; hosnya tidak boleh diubah, dan pengguna ini tidak boleh dipadamkan atau dibatalkan kebenarannya'
|
||||
ErrUpdateBuWebsite: 'Aplikasi telah berjaya dikemas kini, tetapi pengubahsuaian fail konfigurasi tapak web gagal. Sila semak konfigurasi'
|
||||
Err1PanelNetworkFailed: 'Pembuatan rangkaian kontena lalai gagal {{ .detail }}'
|
||||
ErrFileParse: 'Penghuraian fail karang docker aplikasi gagal'
|
||||
|
||||
@@ -110,6 +110,7 @@ ErrAppLimit: 'O número de aplicativos instalados excedeu o limite'
|
||||
ErrNotInstall: 'Aplicativo não instalado'
|
||||
ErrPortInOtherApp: 'A porta {{ .port }} já está ocupada pelo aplicativo {{ .apps }}'
|
||||
ErrDbUserNotValid: 'Banco de dados existente, nome de usuário e senha não correspondem'
|
||||
ErrMysqlUserUsedByApps: 'Este usuário do banco de dados está sendo usado por [{{ .detail }}]; seu host não pode ser alterado, e ele não pode ser excluído nem ter sua autorização revogada'
|
||||
ErrUpdateBuWebsite: 'O aplicativo foi atualizado com sucesso, mas a modificação do arquivo de configuração do site falhou., verifique a configuração'
|
||||
Err1PanelNetworkFailed: 'Falha na criação da rede de contêiner padrão {{ .detalhe }}'
|
||||
ErrFileParse: 'Falha na análise do arquivo docker-compose do aplicativo'
|
||||
|
||||
@@ -110,6 +110,7 @@ ErrAppLimit: 'Количество установленных приложени
|
||||
ErrNotInstall: 'Приложение не установлено'
|
||||
ErrPortInOtherApp: 'Порт {{ .port }} уже занят приложением {{ .apps }}'
|
||||
ErrDbUserNotValid: 'Существующая база данных, имя пользователя и пароль не совпадают'
|
||||
ErrMysqlUserUsedByApps: 'Этот пользователь базы данных используется приложениями [{{ .detail }}]; его хост нельзя изменить, а самого пользователя нельзя удалить или лишить прав'
|
||||
ErrUpdateBuWebsite: 'Приложение успешно обновлено, но изменение файла конфигурации веб-сайта не удалось. Пожалуйста, проверьте конфигурацию'
|
||||
Err1PanelNetworkFailed: 'Создание сети контейнеров по умолчанию не удалось {{ .detail }}'
|
||||
ErrFileParse: 'Ошибка анализа файла docker-compose приложения'
|
||||
|
||||
@@ -110,6 +110,7 @@ ErrAppLimit: 'Yüklenen uygulama sayısı sınırı aştı'
|
||||
ErrNotInstall: 'Uygulama yüklenmedi'
|
||||
ErrPortInOtherApp: '{{ .port }} portu zaten {{ .apps }} uygulaması tarafından kullanılıyor'
|
||||
ErrDbUserNotValid: 'Mevcut veritabanı, kullanıcı adı ve şifre eşleşmiyor'
|
||||
ErrMysqlUserUsedByApps: 'Bu veritabanı kullanıcısı [{{ .detail }}] tarafından kullanılıyor; ana bilgisayarı değiştirilemez, kullanıcı silinemez veya yetkisi kaldırılamaz'
|
||||
ErrUpdateBuWebsite: 'Uygulama başarıyla güncellendi, ancak web sitesi yapılandırma dosyası değiştirme başarısız. Lütfen yapılandırmayı kontrol edin'
|
||||
Err1PanelNetworkFailed: 'Varsayılan konteyner ağ oluşturma başarısız {{ .detail }}'
|
||||
ErrFileParse: 'Uygulama docker-compose dosya ayrıştırma başarısız'
|
||||
|
||||
@@ -110,6 +110,7 @@ ErrAppLimit: '應用程式超出安裝數量限制'
|
||||
ErrNotInstall: '應用程式未安裝'
|
||||
ErrPortInOtherApp: '{{ .port }} 連接埠已被應用程式{{ .apps }} 佔用!'
|
||||
ErrDbUserNotValid: '存量資料庫,使用者名稱密碼不符!'
|
||||
ErrMysqlUserUsedByApps: '該資料庫使用者已被【{{ .detail }}】應用使用,無法修改 Host、刪除或撤銷授權'
|
||||
ErrUpdateBuWebsite: '應用程式更新成功,但網站設定檔修改失敗,請檢查設定!'
|
||||
Err1PanelNetworkFailed: '預設容器網路建立失敗! {{ .detail }}'
|
||||
ErrFileParse: '應用docker-compose 檔解析失敗!'
|
||||
|
||||
@@ -110,6 +110,7 @@ ErrAppLimit: "应用安装数量超限"
|
||||
ErrNotInstall: "应用未安装"
|
||||
ErrPortInOtherApp: "{{ .port }} 端口已被应用 {{ .apps }} 占用!"
|
||||
ErrDbUserNotValid: "存量数据库用户名或密码不匹配"
|
||||
ErrMysqlUserUsedByApps: "该数据库用户已被【{{ .detail }}】应用使用,无法修改 Host、删除或者撤销授权"
|
||||
ErrUpdateBuWebsite: '应用更新成功,但网站配置文件修改失败,请检查配置'
|
||||
Err1PanelNetworkFailed: '默认容器网络创建失败!{{ .detail }}'
|
||||
ErrFileParse: '应用 docker-compose 解析失败'
|
||||
|
||||
@@ -51,6 +51,7 @@ var AddTable = &gormigrate.Migration{
|
||||
&model.Database{},
|
||||
&model.DatabaseMysql{},
|
||||
&model.DatabaseUser{},
|
||||
&model.DatabaseUserGrant{},
|
||||
&model.DatabaseMongodb{},
|
||||
&model.DatabasePostgresql{},
|
||||
&model.Favorite{},
|
||||
@@ -1570,50 +1571,49 @@ func isDatabaseSystemUserForMigration(username string) bool {
|
||||
}
|
||||
}
|
||||
|
||||
func migrateDatabaseUsers(tx *gorm.DB) error {
|
||||
var mysqls []model.DatabaseMysql
|
||||
if err := tx.Find(&mysqls).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, item := range mysqls {
|
||||
if len(item.Username) == 0 || len(item.Password) == 0 || isDatabaseSystemUserForMigration(item.Username) {
|
||||
continue
|
||||
}
|
||||
for _, dbType := range loadDatabaseUserTypesForMigration(tx, item.MysqlName) {
|
||||
if len(dbType) == 0 {
|
||||
continue
|
||||
}
|
||||
for _, host := range normalizeDatabaseUserHostsForMigration(item.Permission) {
|
||||
var old model.DatabaseUser
|
||||
err := tx.Where("`type` = ? AND database = ? AND username = ? AND host = ?", dbType, item.MysqlName, item.Username, host).First(&old).Error
|
||||
if err == nil {
|
||||
continue
|
||||
}
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
if err := tx.Create(&model.DatabaseUser{
|
||||
Type: dbType,
|
||||
Database: item.MysqlName,
|
||||
Username: item.Username,
|
||||
Host: host,
|
||||
Password: item.Password,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var AddDatabaseUserTable = &gormigrate.Migration{
|
||||
ID: "20260703-add-database-user-table",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
if err := tx.AutoMigrate(&model.DatabaseUser{}); err != nil {
|
||||
if err := tx.AutoMigrate(&model.DatabaseUser{}, &model.DatabaseUserGrant{}); err != nil {
|
||||
return err
|
||||
}
|
||||
return migrateDatabaseUsers(tx)
|
||||
var mysqls []model.DatabaseMysql
|
||||
if err := tx.Find(&mysqls).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, item := range mysqls {
|
||||
if len(item.Username) == 0 || isDatabaseSystemUserForMigration(item.Username) {
|
||||
continue
|
||||
}
|
||||
for _, dbType := range loadDatabaseUserTypesForMigration(tx, item.MysqlName) {
|
||||
if len(dbType) == 0 {
|
||||
continue
|
||||
}
|
||||
for _, host := range normalizeDatabaseUserHostsForMigration(item.Permission) {
|
||||
user := model.DatabaseUser{
|
||||
Type: dbType,
|
||||
Database: item.MysqlName,
|
||||
Username: item.Username,
|
||||
Host: host,
|
||||
Password: item.Password,
|
||||
}
|
||||
if err := tx.Where("`type` = ? AND database = ? AND username = ? AND host = ?", dbType, item.MysqlName, item.Username, host).FirstOrCreate(&user).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
grant := model.DatabaseUserGrant{
|
||||
Type: dbType,
|
||||
Database: item.MysqlName,
|
||||
DBName: item.Name,
|
||||
Username: item.Username,
|
||||
Host: host,
|
||||
}
|
||||
if err := tx.Where("`type` = ? AND database = ? AND db_name = ? AND username = ? AND host = ?", dbType, item.MysqlName, item.Name, item.Username, host).FirstOrCreate(&grant).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ func (s *DatabaseRouter) InitRouter(Router *gin.RouterGroup) {
|
||||
cmdRouter.POST("/users", baseApi.CreateMysqlUser)
|
||||
cmdRouter.POST("/users/update", baseApi.UpdateMysqlUser)
|
||||
cmdRouter.POST("/users/password", baseApi.ChangeMysqlUserPassword)
|
||||
cmdRouter.POST("/users/password/save", baseApi.SaveMysqlUserPassword)
|
||||
cmdRouter.POST("/users/del", baseApi.DeleteMysqlUser)
|
||||
cmdRouter.POST("/grants/search", baseApi.ListMysqlGrants)
|
||||
cmdRouter.POST("/grants/summary", baseApi.ListMysqlGrantSummary)
|
||||
|
||||
@@ -15252,6 +15252,52 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/databases/users/password/save": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"description": "request",
|
||||
"in": "body",
|
||||
"name": "request",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.MysqlUserPassword"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
},
|
||||
{
|
||||
"Timestamp": []
|
||||
}
|
||||
],
|
||||
"summary": "Save mysql user password locally",
|
||||
"tags": [
|
||||
"Database Mysql"
|
||||
],
|
||||
"x-panel-log": {
|
||||
"BeforeFunctions": [],
|
||||
"bodyKeys": [
|
||||
"database",
|
||||
"username",
|
||||
"host"
|
||||
],
|
||||
"formatEN": "save mysql database [database] user [username]@[host] password locally",
|
||||
"formatZH": "补充 mysql 数据库 [database] 用户 [username]@[host] 密码",
|
||||
"paramKeys": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"/databases/users/search": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
@@ -35247,6 +35293,9 @@ const docTemplate = `{
|
||||
"ko": {
|
||||
"type": "string"
|
||||
},
|
||||
"lo": {
|
||||
"type": "string"
|
||||
},
|
||||
"ms": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -35299,7 +35348,8 @@ const docTemplate = `{
|
||||
"pt-BR",
|
||||
"tr",
|
||||
"es-ES",
|
||||
"fa"
|
||||
"fa",
|
||||
"lo"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
@@ -36208,6 +36258,9 @@ const docTemplate = `{
|
||||
"host": {
|
||||
"type": "string"
|
||||
},
|
||||
"isDelete": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"password": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -15248,6 +15248,52 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/databases/users/password/save": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"description": "request",
|
||||
"in": "body",
|
||||
"name": "request",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.MysqlUserPassword"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
},
|
||||
{
|
||||
"Timestamp": []
|
||||
}
|
||||
],
|
||||
"summary": "Save mysql user password locally",
|
||||
"tags": [
|
||||
"Database Mysql"
|
||||
],
|
||||
"x-panel-log": {
|
||||
"BeforeFunctions": [],
|
||||
"bodyKeys": [
|
||||
"database",
|
||||
"username",
|
||||
"host"
|
||||
],
|
||||
"formatEN": "save mysql database [database] user [username]@[host] password locally",
|
||||
"formatZH": "补充 mysql 数据库 [database] 用户 [username]@[host] 密码",
|
||||
"paramKeys": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"/databases/users/search": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
@@ -35243,6 +35289,9 @@
|
||||
"ko": {
|
||||
"type": "string"
|
||||
},
|
||||
"lo": {
|
||||
"type": "string"
|
||||
},
|
||||
"ms": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -35295,7 +35344,8 @@
|
||||
"pt-BR",
|
||||
"tr",
|
||||
"es-ES",
|
||||
"fa"
|
||||
"fa",
|
||||
"lo"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
@@ -36204,6 +36254,9 @@
|
||||
"host": {
|
||||
"type": "string"
|
||||
},
|
||||
"isDelete": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"password": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -3120,6 +3120,17 @@
|
||||
"formatZH": "更新 mysql 数据库 [database] 用户 [username]@[host] 密码",
|
||||
"formatEN": "update mysql database [database] user [username]@[host] password"
|
||||
},
|
||||
"/databases/users/password/save": {
|
||||
"bodyKeys": [
|
||||
"database",
|
||||
"username",
|
||||
"host"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "补充 mysql 数据库 [database] 用户 [username]@[host] 密码",
|
||||
"formatEN": "save mysql database [database] user [username]@[host] password locally"
|
||||
},
|
||||
"/databases/users/update": {
|
||||
"bodyKeys": [
|
||||
"database",
|
||||
|
||||
@@ -23,8 +23,9 @@ export namespace Database {
|
||||
username: string;
|
||||
password: string;
|
||||
permission: string;
|
||||
isDelete: string;
|
||||
isDelete: boolean;
|
||||
description: string;
|
||||
authorizedUsers?: MysqlUser[];
|
||||
}
|
||||
export interface BaseInfo {
|
||||
name: string;
|
||||
@@ -56,6 +57,7 @@ export namespace Database {
|
||||
host: string;
|
||||
password: string;
|
||||
description: string;
|
||||
isDelete: boolean;
|
||||
}
|
||||
|
||||
export interface MysqlGrant {
|
||||
|
||||
@@ -79,6 +79,11 @@ export const updateMysqlUserPassword = (params: Database.MysqlUserPassword) => {
|
||||
encodeBase64Fields(request, ['password']);
|
||||
return http.post(`/databases/users/password`, request);
|
||||
};
|
||||
export const saveMysqlUserPassword = (params: Database.MysqlUserPassword) => {
|
||||
let request = deepCopy(params) as Database.MysqlUserPassword;
|
||||
encodeBase64Fields(request, ['password']);
|
||||
return http.post(`/databases/users/password/save`, request);
|
||||
};
|
||||
export const searchMysqlGrants = (params: Database.MysqlUserSearch) => {
|
||||
return http.post<Database.MysqlGrant[]>(`/databases/grants/search`, params);
|
||||
};
|
||||
|
||||
@@ -511,7 +511,7 @@ const message = {
|
||||
delete: 'Delete action cannot be undone, please input "',
|
||||
deleteHelper: '" to delete this database',
|
||||
deleteUserHelper: '" to delete this user',
|
||||
userBoundDatabases: 'is bound to the following databases:',
|
||||
userBoundDatabases: 'has access to the following databases:',
|
||||
noMysql: 'Database service (MySQL or MariaDB)',
|
||||
noPostgresql: 'Database service PostgreSQL',
|
||||
goUpgrade: 'Go to upgrade',
|
||||
@@ -549,11 +549,24 @@ const message = {
|
||||
'This port is the exposed port of the container. You need to save the modification separately and restart the container!',
|
||||
loadFromRemote: 'Sync from server',
|
||||
userBind: 'Bind user',
|
||||
userAuthorization: 'User authorization',
|
||||
noUserAuthorization: 'Do not authorize',
|
||||
authorizedDatabaseCount: '{0} authorized database(s)',
|
||||
authorizationManagement: 'Authorization management',
|
||||
authorizedUsers: 'Authorized users',
|
||||
authorizedUserCount: '{0} user(s)',
|
||||
addUserAuthorization: 'Add user authorization',
|
||||
revokeAuthorization: 'Revoke authorization',
|
||||
revokeAuthorizationHelper: "Revoke user {0}'s access to database {1}?",
|
||||
passwordPendingSupplement: 'Pending',
|
||||
supplementPassword: 'Add password',
|
||||
supplementPasswordHelper: 'This only saves the existing password to 1Panel and does not change it in MySQL.',
|
||||
deleteUserRecordHelper: 'This user has been deleted from MySQL. This only removes the local 1Panel record.',
|
||||
noUserBind: 'No binding',
|
||||
pgBindHelper:
|
||||
'This operation is used to create a new user and bind it to the target database. Currently, selecting users already existing in the database is not supported.',
|
||||
pgSuperUser: 'Superuser',
|
||||
loadFromRemoteHelper: 'Sync database info from the server to 1Panel. Continue?',
|
||||
loadFromRemoteHelper: 'Sync databases, users, and authorizations from the server to 1Panel. Continue?',
|
||||
passwordHelper: 'Not available, click to update',
|
||||
remote: 'Remote',
|
||||
remoteDB: 'Remote server | Remote servers',
|
||||
|
||||
@@ -507,7 +507,7 @@ const message = {
|
||||
deleteBackupHelper: 'Eliminar copias de seguridad de la base de datos simultáneamente',
|
||||
delete: 'La operación de eliminación no se puede revertir, por favor introduzca "',
|
||||
deleteHelper: '" para eliminar esta base de datos',
|
||||
userBoundDatabases: 'está vinculado a las siguientes bases de datos:',
|
||||
userBoundDatabases: 'tiene acceso a las siguientes bases de datos:',
|
||||
noMysql: 'Servicio de base de datos (MySQL o MariaDB)',
|
||||
noPostgresql: 'Servicio de base de datos PostgreSQL',
|
||||
goUpgrade: 'Ir a actualizar',
|
||||
@@ -546,11 +546,25 @@ const message = {
|
||||
'Este puerto es el puerto expuesto del contenedor. Debe guardar la modificación por separado y reiniciar el contenedor.',
|
||||
loadFromRemote: 'Sincronizar desde el servidor',
|
||||
userBind: 'Vincular usuario',
|
||||
userAuthorization: 'Autorización de usuario',
|
||||
noUserAuthorization: 'No autorizar',
|
||||
authorizedDatabaseCount: '{0} base(s) de datos autorizada(s)',
|
||||
authorizationManagement: 'Gestión de autorizaciones',
|
||||
authorizedUsers: 'Usuarios autorizados',
|
||||
authorizedUserCount: '{0} usuario(s)',
|
||||
addUserAuthorization: 'Añadir autorización de usuario',
|
||||
revokeAuthorization: 'Revocar autorización',
|
||||
revokeAuthorizationHelper: '¿Revocar el acceso del usuario {0} a la base de datos {1}?',
|
||||
passwordPendingSupplement: 'Pendiente',
|
||||
supplementPassword: 'Añadir contraseña',
|
||||
supplementPasswordHelper: 'Solo guarda la contraseña existente en 1Panel; no cambia la contraseña en MySQL.',
|
||||
deleteUserRecordHelper:
|
||||
'El usuario ya se eliminó de MySQL. Esta acción solo elimina el registro local de 1Panel.',
|
||||
pgBindHelper:
|
||||
'Esta operación se utiliza para crear un nuevo usuario y vincularlo a la base de datos destino. Actualmente no se admite seleccionar usuarios ya existentes en la base de datos.',
|
||||
pgSuperUser: 'Superusuario',
|
||||
loadFromRemoteHelper:
|
||||
'Esto sincronizará la información de la base de datos del servidor a 1Panel. ¿Desea continuar?',
|
||||
'Esto sincronizará las bases de datos, los usuarios y las autorizaciones del servidor con 1Panel. ¿Desea continuar?',
|
||||
passwordHelper: 'No se puede obtener, haga clic para modificar',
|
||||
remote: 'Remoto',
|
||||
remoteDB: 'Servidor remoto | Servidores remotos',
|
||||
|
||||
@@ -499,7 +499,7 @@ const message = {
|
||||
deleteBackupHelper: 'همزمان پشتیبانهای پایگاه داده را حذف کنید',
|
||||
delete: 'عملیات حذف قابل بازگشت نیست، لطفاً عبارت "',
|
||||
deleteHelper: '" را برای حذف این پایگاه داده وارد کنید',
|
||||
userBoundDatabases: 'به پایگاههای داده زیر متصل است:',
|
||||
userBoundDatabases: 'به پایگاههای داده زیر دسترسی دارد:',
|
||||
noMysql: 'سرویس پایگاه داده (MySQL یا MariaDB)',
|
||||
noPostgresql: 'سرویس پایگاه داده PostgreSQL',
|
||||
goUpgrade: 'رفتن به ارتقاء',
|
||||
@@ -537,10 +537,23 @@ const message = {
|
||||
'این پورت، پورت نمایش داده شده کانتینر است. باید تغییرات را جداگانه ذخیره و کانتینر را مجدداً راهاندازی کنید!',
|
||||
loadFromRemote: 'همگامسازی از سرور',
|
||||
userBind: 'اتصال کاربر',
|
||||
userAuthorization: 'مجوز کاربر',
|
||||
noUserAuthorization: 'بدون اعطای مجوز',
|
||||
authorizedDatabaseCount: '{0} پایگاه داده مجاز',
|
||||
authorizationManagement: 'مدیریت مجوزها',
|
||||
authorizedUsers: 'کاربران مجاز',
|
||||
authorizedUserCount: '{0} کاربر',
|
||||
addUserAuthorization: 'افزودن مجوز کاربر',
|
||||
revokeAuthorization: 'لغو مجوز',
|
||||
revokeAuthorizationHelper: 'دسترسی کاربر {0} به پایگاه داده {1} لغو شود؟',
|
||||
passwordPendingSupplement: 'در انتظار تکمیل',
|
||||
supplementPassword: 'افزودن گذرواژه',
|
||||
supplementPasswordHelper: 'فقط گذرواژه فعلی را در 1Panel ذخیره میکند و گذرواژه MySQL را تغییر نمیدهد.',
|
||||
deleteUserRecordHelper: 'این کاربر از MySQL حذف شده است. این عملیات فقط رکورد محلی 1Panel را حذف میکند.',
|
||||
pgBindHelper:
|
||||
'این عملیات برای ایجاد کاربر جدید و اتصال آن به پایگاه داده هدف استفاده میشود. در حال حاضر انتخاب کاربرانی که از قبل در پایگاه داده وجود دارند پشتیبانی نمیشود.',
|
||||
pgSuperUser: 'کاربر فوقالعاده',
|
||||
loadFromRemoteHelper: 'همگامسازی اطلاعات پایگاه داده از سرور به 1Panel. ادامه میدهید؟',
|
||||
loadFromRemoteHelper: 'پایگاههای داده، کاربران و مجوزها از سرور با 1Panel همگام شوند؟',
|
||||
passwordHelper: 'در دسترس نیست، برای بهروزرسانی کلیک کنید',
|
||||
remote: 'از راه دور',
|
||||
remoteDB: 'سرور از راه دور | سرورهای از راه دور',
|
||||
|
||||
@@ -505,7 +505,7 @@ const message = {
|
||||
deleteBackupHelper: 'データベースのバックアップを同時に削除します',
|
||||
delete: '削除操作はロールバックできません、入力してください」',
|
||||
deleteHelper: '「このデータベースを削除します',
|
||||
userBoundDatabases: 'は次のデータベースにバインドされています:',
|
||||
userBoundDatabases: 'は次のデータベースへのアクセス権があります:',
|
||||
noMysql: 'データベースサービス(mysqlまたはmariadb)',
|
||||
noPostgresql: 'データベースサービスpostgreSql',
|
||||
goUpgrade: 'アップグレードに移動します',
|
||||
@@ -544,10 +544,23 @@ const message = {
|
||||
'このポートは、コンテナの露出したポートです。変更を個別に保存して、コンテナを再起動する必要があります!',
|
||||
loadFromRemote: '同期',
|
||||
userBind: 'バインドユーザー',
|
||||
userAuthorization: 'ユーザー権限',
|
||||
noUserAuthorization: '権限を付与しない',
|
||||
authorizedDatabaseCount: '{0} データベースに権限付与済み',
|
||||
authorizationManagement: '権限管理',
|
||||
authorizedUsers: '権限付与済みユーザー',
|
||||
authorizedUserCount: '{0} ユーザー',
|
||||
addUserAuthorization: 'ユーザー権限を追加',
|
||||
revokeAuthorization: '権限を取り消す',
|
||||
revokeAuthorizationHelper: 'ユーザー {0} のデータベース {1} へのアクセス権を取り消しますか?',
|
||||
passwordPendingSupplement: '未登録',
|
||||
supplementPassword: 'パスワードを登録',
|
||||
supplementPasswordHelper: '既存のパスワードを 1Panel に保存するだけで、MySQL のパスワードは変更しません。',
|
||||
deleteUserRecordHelper: 'このユーザーは MySQL から削除済みです。1Panel のローカル記録のみ削除します。',
|
||||
pgBindHelper:
|
||||
'この操作は、新しいユーザーを作成し、ターゲットデータベースにバインドするために使用されます。現在、データベースに既存のユーザーを選択することはサポートされていません。',
|
||||
pgSuperUser: 'スーパーユーザー',
|
||||
loadFromRemoteHelper: 'これにより、サーバー上のデータベース情報が1パネルに同期します。続けたいですか?',
|
||||
loadFromRemoteHelper: 'サーバー上のデータベース、ユーザー、権限情報を 1Panel に同期します。続行しますか?',
|
||||
passwordHelper: '取得できません、クリックして修正',
|
||||
remote: 'リモート',
|
||||
remoteDB: 'リモートサーバー|リモートサーバー',
|
||||
|
||||
@@ -500,7 +500,7 @@ const message = {
|
||||
deleteBackupHelper: '데이터베이스 백업을 동시에 삭제',
|
||||
delete: '삭제 작업은 되돌릴 수 없습니다. 삭제하려면 "',
|
||||
deleteHelper: '"를 입력하세요.',
|
||||
userBoundDatabases: '다음 데이터베이스에 바인딩되어 있습니다:',
|
||||
userBoundDatabases: '다음 데이터베이스에 대한 접근 권한이 있습니다:',
|
||||
noMysql: '데이터베이스 서비스 (MySQL 또는 MariaDB)',
|
||||
noPostgresql: '데이터베이스 서비스 PostgreSQL',
|
||||
goUpgrade: '업그레이드로 이동',
|
||||
@@ -538,10 +538,23 @@ const message = {
|
||||
portHelper: '이 포트는 컨테이너의 노출된 포트입니다. 수정을 별도로 저장하고 컨테이너를 재시작해야 합니다!',
|
||||
loadFromRemote: '동기화',
|
||||
userBind: '사용자 바인딩',
|
||||
userAuthorization: '사용자 권한',
|
||||
noUserAuthorization: '권한 부여 안 함',
|
||||
authorizedDatabaseCount: '데이터베이스 {0}개 권한 보유',
|
||||
authorizationManagement: '권한 관리',
|
||||
authorizedUsers: '권한이 부여된 사용자',
|
||||
authorizedUserCount: '사용자 {0}명',
|
||||
addUserAuthorization: '사용자 권한 추가',
|
||||
revokeAuthorization: '권한 취소',
|
||||
revokeAuthorizationHelper: '사용자 {0}의 데이터베이스 {1} 접근 권한을 취소하시겠습니까?',
|
||||
passwordPendingSupplement: '입력 필요',
|
||||
supplementPassword: '비밀번호 추가',
|
||||
supplementPasswordHelper: '기존 비밀번호를 1Panel에만 저장하며 MySQL 비밀번호는 변경하지 않습니다.',
|
||||
deleteUserRecordHelper: '이 사용자는 MySQL에서 이미 삭제되었습니다. 1Panel의 로컬 기록만 삭제합니다.',
|
||||
pgBindHelper:
|
||||
'이 작업은 새 사용자를 생성하여 대상 데이터베이스에 바인딩하는 데 사용됩니다. 현재 데이터베이스에 이미 존재하는 사용자 선택은 지원되지 않습니다.',
|
||||
pgSuperUser: '슈퍼 사용자',
|
||||
loadFromRemoteHelper: '이 작업은 서버의 데이터베이스 정보를 1Panel로 동기화합니다. 계속 진행하시겠습니까?',
|
||||
loadFromRemoteHelper: '서버의 데이터베이스, 사용자 및 권한 정보를 1Panel로 동기화합니다. 계속하시겠습니까?',
|
||||
passwordHelper: '확인 불가, 수정하려면 클릭',
|
||||
remote: '원격',
|
||||
remoteDB: '원격 서버 | 원격 서버들',
|
||||
|
||||
@@ -509,7 +509,7 @@ const message = {
|
||||
delete: 'ການລຶບບໍ່ສາມາດຍ້ອນກັບໄດ້, ກະລຸນາປ້ອນ "',
|
||||
deleteHelper: '" ເພື່ອລຶບຖານຂໍ້ມູນນີ້',
|
||||
deleteUserHelper: '" ເພື່ອລຶບຜູ້ໃຊ້ນີ້',
|
||||
userBoundDatabases: 'ຖືກຜູກກັບຖານຂໍ້ມູນຕໍ່ໄປນີ້:',
|
||||
userBoundDatabases: 'ມີສິດເຂົ້າເຖິງຖານຂໍ້ມູນຕໍ່ໄປນີ້:',
|
||||
noMysql: 'ການບໍລິການຖານຂໍ້ມູນ (MySQL ຫຼື MariaDB)',
|
||||
noPostgresql: 'ການບໍລິການຖານຂໍ້ມູນ PostgreSQL',
|
||||
goUpgrade: 'ໄປອັບເກຣດ',
|
||||
@@ -545,11 +545,24 @@ const message = {
|
||||
portHelper: 'ພອດນີ້ແມ່ນພອດທີ່ເປີດອອກຂອງຄອນເທນເນີ. ທ່ານຕ້ອງບັນທຶກການແກ້ໄຂແຍກຕ່າງຫາກ ແລະ ຣີສະຕາດຄອນເທນເນີ!',
|
||||
loadFromRemote: 'ຊິ້ງຄ໌ຈາກເຊີເວີ',
|
||||
userBind: 'ຜູກມັດຜູ້ໃຊ້',
|
||||
userAuthorization: 'ການໃຫ້ສິດຜູ້ໃຊ້',
|
||||
noUserAuthorization: 'ບໍ່ໃຫ້ສິດ',
|
||||
authorizedDatabaseCount: 'ໄດ້ຮັບສິດ {0} ຖານຂໍ້ມູນ',
|
||||
authorizationManagement: 'ຈັດການສິດ',
|
||||
authorizedUsers: 'ຜູ້ໃຊ້ທີ່ໄດ້ຮັບສິດ',
|
||||
authorizedUserCount: '{0} ຜູ້ໃຊ້',
|
||||
addUserAuthorization: 'ເພີ່ມສິດຜູ້ໃຊ້',
|
||||
revokeAuthorization: 'ຖອນສິດ',
|
||||
revokeAuthorizationHelper: 'ຖອນສິດຜູ້ໃຊ້ {0} ໃນຖານຂໍ້ມູນ {1} ຫຼືບໍ່?',
|
||||
passwordPendingSupplement: 'ລໍຖ້າເພີ່ມ',
|
||||
supplementPassword: 'ເພີ່ມລະຫັດຜ່ານ',
|
||||
supplementPasswordHelper: 'ບັນທຶກລະຫັດຜ່ານປັດຈຸບັນໄວ້ໃນ 1Panel ເທົ່ານັ້ນ ແລະບໍ່ປ່ຽນລະຫັດ MySQL.',
|
||||
deleteUserRecordHelper: 'ຜູ້ໃຊ້ນີ້ຖືກລຶບຈາກ MySQL ແລ້ວ. ຈະລຶບສະເພາະຂໍ້ມູນທ້ອງຖິ່ນໃນ 1Panel.',
|
||||
noUserBind: 'ບໍ່ມີການຜູກມັດ',
|
||||
pgBindHelper:
|
||||
'ການດຳເນີນການນີ້ໃຊ້ເພື່ອສ້າງຜູ້ໃຊ້ໃໝ່ ແລະ ຜູກມັດກັບຖານຂໍ້ມູນເປົ້າໝາຍ. ໃນປັດຈຸບັນ, ຍັງບໍ່ຮອງຮັບການເລືອກຜູ້ໃຊ້ທີ່ມີຢູ່ແລ້ວໃນຖານຂໍ້ມູນ.',
|
||||
pgSuperUser: 'ຊຸບເປີຢູເຊີ',
|
||||
loadFromRemoteHelper: 'ຊິ້ງຄ໌ຂໍ້ມູນຖານຂໍ້ມູນຈາກເຊີເວີມາຍັງ 1Panel. ຕ້ອງການຕໍ່ຫຼືບໍ່?',
|
||||
loadFromRemoteHelper: 'ຊິ້ງຄ໌ຖານຂໍ້ມູນ, ຜູ້ໃຊ້ ແລະສິດຈາກເຊີເວີມາຍັງ 1Panel ຫຼືບໍ່?',
|
||||
passwordHelper: 'ບໍ່ສາມາດໃຊ້ໄດ້, ຄລິກເພື່ອອັບເດດ',
|
||||
remote: 'ທາງໄກ',
|
||||
remoteDB: 'ເຊີເວີທາງໄກ | ເຊີເວີທາງໄກ',
|
||||
|
||||
@@ -510,7 +510,7 @@ const message = {
|
||||
deleteBackupHelper: 'Padam sandaran pangkalan data secara serentak',
|
||||
delete: 'Operasi padam tidak boleh diundurkan, sila masukkan "',
|
||||
deleteHelper: '" untuk memadam pangkalan data ini',
|
||||
userBoundDatabases: 'terikat dengan pangkalan data berikut:',
|
||||
userBoundDatabases: 'mempunyai akses kepada pangkalan data berikut:',
|
||||
noMysql: 'Perkhidmatan pangkalan data (MySQL atau MariaDB)',
|
||||
noPostgresql: 'Perkhidmatan pangkalan data PostgreSQL',
|
||||
goUpgrade: 'Pergi tingkatkan',
|
||||
@@ -549,11 +549,25 @@ const message = {
|
||||
'Port ini adalah port yang didedahkan oleh kontena. Anda perlu menyimpan pengubahsuaian secara berasingan dan memulakan semula kontena!',
|
||||
loadFromRemote: 'Selaras',
|
||||
userBind: 'Kaitkan pengguna',
|
||||
userAuthorization: 'Kebenaran pengguna',
|
||||
noUserAuthorization: 'Jangan beri kebenaran',
|
||||
authorizedDatabaseCount: '{0} pangkalan data dibenarkan',
|
||||
authorizationManagement: 'Pengurusan kebenaran',
|
||||
authorizedUsers: 'Pengguna yang dibenarkan',
|
||||
authorizedUserCount: '{0} pengguna',
|
||||
addUserAuthorization: 'Tambah kebenaran pengguna',
|
||||
revokeAuthorization: 'Tarik balik kebenaran',
|
||||
revokeAuthorizationHelper: 'Tarik balik akses pengguna {0} kepada pangkalan data {1}?',
|
||||
passwordPendingSupplement: 'Belum dilengkapkan',
|
||||
supplementPassword: 'Tambah kata laluan',
|
||||
supplementPasswordHelper: 'Hanya menyimpan kata laluan sedia ada ke 1Panel dan tidak mengubahnya dalam MySQL.',
|
||||
deleteUserRecordHelper:
|
||||
'Pengguna ini telah dipadam daripada MySQL. Tindakan ini hanya memadam rekod setempat 1Panel.',
|
||||
pgBindHelper:
|
||||
'Operasi ini digunakan untuk mencipta pengguna baharu dan mengaitkannya dengan pangkalan data sasaran. Pada masa ini, memilih pengguna yang sudah wujud dalam pangkalan data tidak disokong.',
|
||||
pgSuperUser: 'Pengguna Super',
|
||||
loadFromRemoteHelper:
|
||||
'Ini akan menyelaraskan maklumat pangkalan data di pelayan ke 1Panel. Adakah anda mahu meneruskan?',
|
||||
'Ini akan menyelaraskan pangkalan data, pengguna dan kebenaran dari pelayan ke 1Panel. Teruskan?',
|
||||
passwordHelper: 'Tidak dapat diperoleh, klik untuk ubah',
|
||||
remote: 'Jauh',
|
||||
remoteDB: 'Pelayan jauh | Pelayan-pelayan jauh',
|
||||
|
||||
@@ -504,7 +504,7 @@ const message = {
|
||||
deleteBackupHelper: 'Excluir backups do banco de dados simultaneamente',
|
||||
delete: 'A operação de exclusão não pode ser desfeita, insira "',
|
||||
deleteHelper: '" para excluir este banco de dados',
|
||||
userBoundDatabases: 'vinculado aos seguintes bancos de dados:',
|
||||
userBoundDatabases: 'tem acesso aos seguintes bancos de dados:',
|
||||
noMysql: 'Serviço de banco de dados (MySQL ou MariaDB)',
|
||||
noPostgresql: 'Serviço de banco de dados PostgreSQL',
|
||||
goUpgrade: 'Ir para atualização',
|
||||
@@ -543,11 +543,25 @@ const message = {
|
||||
'Esta porta é a porta exposta do container. Você precisa salvar a modificação separadamente e reiniciar o container!',
|
||||
loadFromRemote: 'Sincronizar',
|
||||
userBind: 'Vincular usuário',
|
||||
userAuthorization: 'Autorização de usuário',
|
||||
noUserAuthorization: 'Não autorizar',
|
||||
authorizedDatabaseCount: '{0} banco(s) de dados autorizado(s)',
|
||||
authorizationManagement: 'Gerenciamento de autorizações',
|
||||
authorizedUsers: 'Usuários autorizados',
|
||||
authorizedUserCount: '{0} usuário(s)',
|
||||
addUserAuthorization: 'Adicionar autorização de usuário',
|
||||
revokeAuthorization: 'Revogar autorização',
|
||||
revokeAuthorizationHelper: 'Revogar o acesso do usuário {0} ao banco de dados {1}?',
|
||||
passwordPendingSupplement: 'Pendente',
|
||||
supplementPassword: 'Adicionar senha',
|
||||
supplementPasswordHelper: 'Apenas salva a senha existente no 1Panel; não altera a senha no MySQL.',
|
||||
deleteUserRecordHelper:
|
||||
'O usuário já foi excluído do MySQL. Esta ação remove apenas o registro local do 1Panel.',
|
||||
pgBindHelper:
|
||||
'Esta operação cria um novo usuário e o vincula ao banco de dados alvo. A seleção de usuários já existentes no banco de dados não é suportada.',
|
||||
pgSuperUser: 'Superusuário',
|
||||
loadFromRemoteHelper:
|
||||
'Isso sincronizará as informações do banco de dados no servidor para o 1Panel. Deseja continuar?',
|
||||
'Isso sincronizará bancos de dados, usuários e autorizações do servidor com o 1Panel. Deseja continuar?',
|
||||
passwordHelper: 'Não é possível obter, clique para modificar',
|
||||
remote: 'Remoto',
|
||||
remoteDB: 'Servidor remoto | Servidores remotos',
|
||||
|
||||
@@ -501,7 +501,7 @@ const message = {
|
||||
deleteBackupHelper: 'Удалить резервные копии базы данных одновременно',
|
||||
delete: 'Операция удаления не может быть отменена, пожалуйста, введите "',
|
||||
deleteHelper: '" для удаления этой базы данных',
|
||||
userBoundDatabases: 'привязан к следующим базам данных:',
|
||||
userBoundDatabases: 'имеет доступ к следующим базам данных:',
|
||||
noMysql: 'Сервис базы данных (MySQL или MariaDB)',
|
||||
noPostgresql: 'Сервис базы данных PostgreSQL',
|
||||
goUpgrade: 'Обновить',
|
||||
@@ -537,10 +537,23 @@ const message = {
|
||||
'Этот порт является открытым портом контейнера. Вам нужно сохранить изменение отдельно и перезапустить контейнер!',
|
||||
loadFromRemote: 'Синхронизировать',
|
||||
userBind: 'Привязать пользователя',
|
||||
userAuthorization: 'Доступ пользователя',
|
||||
noUserAuthorization: 'Не предоставлять доступ',
|
||||
authorizedDatabaseCount: 'Доступных баз данных: {0}',
|
||||
authorizationManagement: 'Управление доступом',
|
||||
authorizedUsers: 'Авторизованные пользователи',
|
||||
authorizedUserCount: 'Пользователей: {0}',
|
||||
addUserAuthorization: 'Добавить доступ пользователю',
|
||||
revokeAuthorization: 'Отозвать доступ',
|
||||
revokeAuthorizationHelper: 'Отозвать у пользователя {0} доступ к базе данных {1}?',
|
||||
passwordPendingSupplement: 'Требуется пароль',
|
||||
supplementPassword: 'Добавить пароль',
|
||||
supplementPasswordHelper: 'Сохраняет текущий пароль только в 1Panel и не изменяет пароль в MySQL.',
|
||||
deleteUserRecordHelper: 'Пользователь уже удалён из MySQL. Будет удалена только локальная запись 1Panel.',
|
||||
pgBindHelper:
|
||||
'Эта операция используется для создания нового пользователя и привязки его к целевой базе данных. В настоящее время выбор уже существующих пользователей в базе данных не поддерживается.',
|
||||
pgSuperUser: 'Суперпользователь',
|
||||
loadFromRemoteHelper: 'Это синхронизирует информацию о базе данных на сервере с 1Panel. Хотите продолжить?',
|
||||
loadFromRemoteHelper: 'Синхронизировать базы данных, пользователей и права доступа с сервера в 1Panel?',
|
||||
passwordHelper: 'Не удается получить, нажмите для изменения',
|
||||
remote: 'Удаленный',
|
||||
remoteDB: 'Удаленный сервер | Удаленные серверы',
|
||||
|
||||
@@ -505,7 +505,7 @@ const message = {
|
||||
deleteBackupHelper: 'Veritabanı yedeklerini aynı anda sil',
|
||||
delete: 'Silme işlemi geri alınamaz, lütfen "',
|
||||
deleteHelper: '" girerek bu veritabanını silin',
|
||||
userBoundDatabases: 'aşağıdaki veritabanlarına bağlı:',
|
||||
userBoundDatabases: 'aşağıdaki veritabanlarına erişebilir:',
|
||||
noMysql: 'Veritabanı hizmeti (MySQL veya MariaDB)',
|
||||
noPostgresql: 'Veritabanı hizmeti PostgreSQL',
|
||||
goUpgrade: 'Yükseltmeye git',
|
||||
@@ -544,11 +544,24 @@ const message = {
|
||||
'Bu port konteynerin açığa çıkan portudur. Değişikliği ayrı olarak kaydetmeniz ve konteyneri yeniden başlatmanız gerekir!',
|
||||
loadFromRemote: 'Sunucudan senkronize et',
|
||||
userBind: 'Kullanıcı bağla',
|
||||
userAuthorization: 'Kullanıcı yetkilendirmesi',
|
||||
noUserAuthorization: 'Yetki verme',
|
||||
authorizedDatabaseCount: '{0} veritabanı yetkisi',
|
||||
authorizationManagement: 'Yetkilendirme yönetimi',
|
||||
authorizedUsers: 'Yetkilendirilmiş kullanıcılar',
|
||||
authorizedUserCount: '{0} kullanıcı',
|
||||
addUserAuthorization: 'Kullanıcı yetkisi ekle',
|
||||
revokeAuthorization: 'Yetkiyi kaldır',
|
||||
revokeAuthorizationHelper: '{0} kullanıcısının {1} veritabanına erişimi kaldırılsın mı?',
|
||||
passwordPendingSupplement: 'Bekliyor',
|
||||
supplementPassword: 'Parola ekle',
|
||||
supplementPasswordHelper: 'Yalnızca mevcut parolayı 1Panel içine kaydeder; MySQL parolasını değiştirmez.',
|
||||
deleteUserRecordHelper: 'Bu kullanıcı MySQL üzerinden silinmiş. Bu işlem yalnızca yerel 1Panel kaydını siler.',
|
||||
pgBindHelper:
|
||||
'Bu işlem yeni bir kullanıcı oluşturmak ve hedef veritabanına bağlamak için kullanılır. Şu anda veritabanında mevcut olan kullanıcıları seçmek desteklenmiyor.',
|
||||
pgSuperUser: 'Süper Kullanıcı',
|
||||
loadFromRemoteHelper:
|
||||
'Bu, sunucudaki veritabanı bilgilerini 1Panele senkronize edecek. Devam etmek istiyor musunuz?',
|
||||
'Sunucudaki veritabanlarını, kullanıcıları ve yetkilendirmeleri 1Panel ile eşitler. Devam edilsin mi?',
|
||||
passwordHelper: 'Alınamıyor, değiştirmek için tıklayın',
|
||||
remote: 'Uzak',
|
||||
remoteDB: 'Uzak sunucu | Uzak sunucular',
|
||||
|
||||
@@ -487,7 +487,7 @@ const message = {
|
||||
deleteBackupHelper: '同時刪除資料庫備份',
|
||||
delete: '刪除操作無法回滾,請輸入 "',
|
||||
deleteHelper: '" 刪除此資料庫',
|
||||
userBoundDatabases: '使用者已綁定如下資料庫:',
|
||||
userBoundDatabases: '使用者已取得以下資料庫的存取授權:',
|
||||
noMysql: '資料庫服務 (MySQL 或 MariaDB)',
|
||||
noPostgresql: '資料庫服務 PostgreSQL',
|
||||
goUpgrade: '去應用列表升級',
|
||||
@@ -521,9 +521,22 @@ const message = {
|
||||
confNotFound: '未能找到該應用設定檔,請在應用商店升級該應用至最新版本後重試',
|
||||
loadFromRemote: '從伺服器同步',
|
||||
userBind: '綁定使用者',
|
||||
userAuthorization: '使用者授權',
|
||||
noUserAuthorization: '不授權',
|
||||
authorizedDatabaseCount: '已授權 {0} 個資料庫',
|
||||
authorizationManagement: '授權管理',
|
||||
authorizedUsers: '已授權使用者',
|
||||
authorizedUserCount: '{0} 個使用者',
|
||||
addUserAuthorization: '新增使用者授權',
|
||||
revokeAuthorization: '撤銷授權',
|
||||
revokeAuthorizationHelper: '確定撤銷使用者 {0} 對資料庫 {1} 的存取授權嗎?',
|
||||
passwordPendingSupplement: '待補充',
|
||||
supplementPassword: '補充密碼',
|
||||
supplementPasswordHelper: '僅將現有密碼儲存到 1Panel,不會修改 MySQL 使用者密碼。',
|
||||
deleteUserRecordHelper: '該使用者已從 MySQL 中刪除,此操作僅刪除 1Panel 中的本機記錄。',
|
||||
pgBindHelper: '此操作用於建立新使用者並將其綁定到目標資料庫,暫不支援選擇已存在於資料庫中的使用者。',
|
||||
pgSuperUser: '超級使用者',
|
||||
loadFromRemoteHelper: '此操作將同步伺服器上資料庫資訊到 1Panel,是否繼續?',
|
||||
loadFromRemoteHelper: '此操作將同步伺服器上的資料庫、使用者及授權資訊到 1Panel,是否繼續?',
|
||||
passwordHelper: '無法取得,可點選修改',
|
||||
remote: '遠端',
|
||||
remoteDB: '遠端伺服器',
|
||||
|
||||
@@ -476,7 +476,7 @@ const message = {
|
||||
delete: '删除操作无法回滚,请输入 "',
|
||||
deleteHelper: '" 删除此数据库',
|
||||
deleteUserHelper: '" 删除此用户',
|
||||
userBoundDatabases: '用户已绑定如下数据库:',
|
||||
userBoundDatabases: '用户已获得以下数据库的访问授权:',
|
||||
noMysql: '数据库服务 (MySQL 或 MariaDB)',
|
||||
noPostgresql: '数据库服务 PostgreSQL',
|
||||
goUpgrade: '去应用列表升级',
|
||||
@@ -513,10 +513,23 @@ const message = {
|
||||
|
||||
loadFromRemote: '从服务器同步',
|
||||
userBind: '绑定用户',
|
||||
userAuthorization: '用户授权',
|
||||
noUserAuthorization: '不授权',
|
||||
authorizedDatabaseCount: '已授权 {0} 个数据库',
|
||||
authorizationManagement: '授权管理',
|
||||
authorizedUsers: '已授权用户',
|
||||
authorizedUserCount: '{0} 个用户',
|
||||
addUserAuthorization: '添加用户授权',
|
||||
revokeAuthorization: '撤销授权',
|
||||
revokeAuthorizationHelper: '确定撤销用户 {0} 对数据库 {1} 的访问授权吗?',
|
||||
passwordPendingSupplement: '待补充',
|
||||
supplementPassword: '补充密码',
|
||||
supplementPasswordHelper: '仅将现有密码保存到 1Panel,不会修改 MySQL 用户密码。',
|
||||
deleteUserRecordHelper: '该用户已从 MySQL 中删除,此操作仅删除 1Panel 中的本地记录。',
|
||||
noUserBind: '不绑定',
|
||||
pgBindHelper: '该操作用于创建新用户并将其绑定到目标数据库,暂不支持选择已存在于数据库中的用户。',
|
||||
pgSuperUser: '超级用户',
|
||||
loadFromRemoteHelper: '此操作将同步服务器上数据库信息到 1Panel,是否继续?',
|
||||
loadFromRemoteHelper: '此操作将同步服务器上的数据库、用户及授权信息到 1Panel,是否继续?',
|
||||
passwordHelper: '无法获取,可点击修改',
|
||||
remote: '远程',
|
||||
remoteDB: '远程服务器',
|
||||
|
||||
@@ -1,5 +1,41 @@
|
||||
<template>
|
||||
<DialogPro v-model="dialogVisible" :title="$t('database.userBind')" size="small">
|
||||
<DialogPro v-model="dialogVisible" :title="$t('database.authorizationManagement')" size="small">
|
||||
<div v-loading="loading">
|
||||
<div class="authorization-toolbar">
|
||||
<el-button type="primary" @click="openAddDialog">
|
||||
{{ $t('database.addUserAuthorization') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table :data="authorizedUsers" :empty-text="$t('commons.msg.noneData')">
|
||||
<el-table-column :label="$t('commons.login.username')" min-width="140">
|
||||
<template #default="{ row }">{{ row.username }}@{{ row.host }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('database.permission')" min-width="110">
|
||||
<template #default="{ row }">{{ permissionLabel(row.host) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('commons.table.description')"
|
||||
prop="description"
|
||||
show-overflow-tooltip
|
||||
min-width="120"
|
||||
/>
|
||||
<el-table-column :label="$t('commons.table.operate')" width="100" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="revokeAuthorization(row)">
|
||||
{{ $t('database.revokeAuthorization') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false" :disabled="loading">
|
||||
{{ $t('commons.button.close') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</DialogPro>
|
||||
|
||||
<DialogPro v-model="addDialogVisible" :title="$t('database.addUserAuthorization')" size="small">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" v-loading="loading">
|
||||
<el-form-item :label="$t('commons.table.type')" prop="mode">
|
||||
<el-radio-group v-model="form.mode" @change="changeMode">
|
||||
@@ -19,7 +55,7 @@
|
||||
@change="syncUser"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in users"
|
||||
v-for="item in availableUsers"
|
||||
:key="item.username + '@' + item.host"
|
||||
class="user-select-option"
|
||||
:label="item.username + ' - ' + permissionLabel(item.host)"
|
||||
@@ -42,7 +78,7 @@
|
||||
>
|
||||
<template #reference>
|
||||
<el-button class="user-bound-button" size="small" @click.stop>
|
||||
{{ $t('commons.status.bound') }} {{ userDatabases(item).length }}
|
||||
{{ $t('database.authorizedDatabaseCount', [userDatabases(item).length]) }}
|
||||
</el-button>
|
||||
</template>
|
||||
<div class="user-bound-list">
|
||||
@@ -82,10 +118,10 @@
|
||||
</template>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false" :disabled="loading">
|
||||
<el-button @click="addDialogVisible = false" :disabled="loading">
|
||||
{{ $t('commons.button.cancel') }}
|
||||
</el-button>
|
||||
<el-button type="primary" @click="submit()" :disabled="loading">
|
||||
<el-button type="primary" @click="submit" :disabled="loading">
|
||||
{{ $t('commons.button.confirm') }}
|
||||
</el-button>
|
||||
</template>
|
||||
@@ -93,16 +129,24 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from 'vue';
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import i18n from '@/lang';
|
||||
import { Rules } from '@/global/form-rules';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
import { Database } from '@/api/interface/database';
|
||||
import { createMysqlUser, grantMysqlUser, searchMysqlGrants, searchMysqlUsers } from '@/api/modules/database';
|
||||
import {
|
||||
createMysqlUser,
|
||||
grantMysqlUser,
|
||||
revokeMysqlGrant,
|
||||
searchMysqlGrants,
|
||||
searchMysqlUsers,
|
||||
} from '@/api/modules/database';
|
||||
|
||||
const emit = defineEmits<{ (e: 'search'): void }>();
|
||||
|
||||
const dialogVisible = ref(false);
|
||||
const addDialogVisible = ref(false);
|
||||
const loading = ref(false);
|
||||
const users = ref<Database.MysqlUser[]>([]);
|
||||
const grants = ref<Database.MysqlGrant[]>([]);
|
||||
@@ -131,14 +175,29 @@ interface DialogProps {
|
||||
db: string;
|
||||
}
|
||||
|
||||
const loadUsers = async () => {
|
||||
const res = await searchMysqlUsers({ database: form.database });
|
||||
users.value = res.data || [];
|
||||
};
|
||||
const grantKeys = computed(
|
||||
() =>
|
||||
new Set(
|
||||
grants.value.filter((item) => item.database === form.db).map((item) => `${item.username}@${item.host}`),
|
||||
),
|
||||
);
|
||||
const authorizedUsers = computed(() =>
|
||||
users.value.filter((item) => !item.isDelete && grantKeys.value.has(`${item.username}@${item.host}`)),
|
||||
);
|
||||
const availableUsers = computed(() =>
|
||||
users.value.filter((item) => !item.isDelete && !grantKeys.value.has(`${item.username}@${item.host}`)),
|
||||
);
|
||||
|
||||
const loadGrants = async () => {
|
||||
const res = await searchMysqlGrants({ database: form.database });
|
||||
grants.value = res.data || [];
|
||||
const loadContext = async () => {
|
||||
if (!form.database) {
|
||||
return;
|
||||
}
|
||||
const [userRes, grantRes] = await Promise.all([
|
||||
searchMysqlUsers({ database: form.database }),
|
||||
searchMysqlGrants({ database: form.database }),
|
||||
]);
|
||||
users.value = userRes.data || [];
|
||||
grants.value = grantRes.data || [];
|
||||
};
|
||||
|
||||
const permissionLabel = (host: string) => {
|
||||
@@ -163,32 +222,43 @@ const changePermission = () => {
|
||||
|
||||
const changeMode = () => {
|
||||
if (form.mode === 'create') {
|
||||
form.username = '';
|
||||
form.host = '%';
|
||||
form.permission = '%';
|
||||
form.password = '';
|
||||
form.description = '';
|
||||
return;
|
||||
}
|
||||
const user = availableUsers.value[0];
|
||||
form.userKey = user ? `${user.username}@${user.host}` : '';
|
||||
syncUser();
|
||||
};
|
||||
|
||||
const acceptParams = async (params: DialogProps) => {
|
||||
form.database = params.database;
|
||||
form.db = params.db;
|
||||
const openAddDialog = () => {
|
||||
form.mode = availableUsers.value.length ? 'select' : 'create';
|
||||
form.password = '';
|
||||
form.description = '';
|
||||
await Promise.all([loadUsers(), loadGrants()]);
|
||||
form.mode = users.value.length ? 'select' : 'create';
|
||||
const user = users.value?.[0];
|
||||
form.userKey = user ? `${user.username}@${user.host}` : '';
|
||||
form.username = user?.username || '';
|
||||
form.host = user?.host || '%';
|
||||
form.permission = form.mode === 'create' || form.host === '%' ? '%' : 'ip';
|
||||
changeMode();
|
||||
addDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const acceptParams = async (params: DialogProps) => {
|
||||
if (!params.database || !params.db) {
|
||||
return;
|
||||
}
|
||||
form.database = params.database;
|
||||
form.db = params.db;
|
||||
dialogVisible.value = true;
|
||||
loading.value = true;
|
||||
try {
|
||||
await loadContext();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const syncUser = () => {
|
||||
const item = users.value.find((item) => `${item.username}@${item.host}` === form.userKey);
|
||||
const item = availableUsers.value.find((item) => `${item.username}@${item.host}` === form.userKey);
|
||||
if (item) {
|
||||
form.username = item.username;
|
||||
form.host = item.host;
|
||||
@@ -216,8 +286,36 @@ const submit = async () => {
|
||||
username: form.username,
|
||||
host: form.host,
|
||||
});
|
||||
await loadContext();
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
addDialogVisible.value = false;
|
||||
emit('search');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const revokeAuthorization = (user: Database.MysqlUser) => {
|
||||
ElMessageBox.confirm(
|
||||
i18n.global.t('database.revokeAuthorizationHelper', [`${user.username}@${user.host}`, form.db]),
|
||||
i18n.global.t('commons.msg.infoTitle'),
|
||||
{
|
||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||
type: 'warning',
|
||||
},
|
||||
).then(async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
await revokeMysqlGrant({
|
||||
database: form.database,
|
||||
db: form.db,
|
||||
username: user.username,
|
||||
host: user.host,
|
||||
});
|
||||
await loadContext();
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
dialogVisible.value = false;
|
||||
emit('search');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
@@ -231,6 +329,10 @@ defineExpose({
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.authorization-toolbar {
|
||||
display: flex;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
:deep(.user-select-option) {
|
||||
height: auto;
|
||||
min-height: 48px;
|
||||
|
||||
@@ -20,12 +20,12 @@
|
||||
</el-select>
|
||||
<span class="input-help">{{ $t('database.collationHelper', [form.format]) }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('database.userBind')" prop="userMode">
|
||||
<el-form-item :label="$t('database.userAuthorization')" prop="userMode">
|
||||
<el-radio-group v-model="form.userMode" @change="changeUserMode">
|
||||
<el-radio-button value="none">
|
||||
{{ $t('database.noUserBind') }}
|
||||
{{ $t('database.noUserAuthorization') }}
|
||||
</el-radio-button>
|
||||
<el-radio-button value="select" :disabled="!users.length">
|
||||
<el-radio-button value="select" :disabled="!activeUsers.length">
|
||||
{{ $t('commons.button.select') }}
|
||||
</el-radio-button>
|
||||
<el-radio-button value="create">
|
||||
@@ -41,7 +41,7 @@
|
||||
@change="syncUser"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in users"
|
||||
v-for="item in activeUsers"
|
||||
:key="item.username + '@' + item.host"
|
||||
class="user-select-option"
|
||||
:label="item.username + ' - ' + permissionLabel(item.host)"
|
||||
@@ -64,7 +64,7 @@
|
||||
>
|
||||
<template #reference>
|
||||
<el-button class="user-bound-button" size="small" @click.stop>
|
||||
{{ $t('commons.status.bound') }} {{ userDatabases(item).length }}
|
||||
{{ $t('database.authorizedDatabaseCount', [userDatabases(item).length]) }}
|
||||
</el-button>
|
||||
</template>
|
||||
<div class="user-bound-list">
|
||||
@@ -131,7 +131,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from 'vue';
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import { Rules } from '@/global/form-rules';
|
||||
import i18n from '@/lang';
|
||||
import { ElForm } from 'element-plus';
|
||||
@@ -151,6 +151,7 @@ const formatOptions = ref();
|
||||
const collationOptions = ref();
|
||||
const users = ref<Database.MysqlUser[]>([]);
|
||||
const grants = ref<Database.MysqlGrant[]>([]);
|
||||
const activeUsers = computed(() => users.value.filter((item) => !item.isDelete));
|
||||
const form = reactive({
|
||||
name: '',
|
||||
from: 'local',
|
||||
@@ -188,6 +189,9 @@ interface DialogProps {
|
||||
database: string;
|
||||
}
|
||||
const acceptParams = async (params: DialogProps): Promise<void> => {
|
||||
if (!params.database) {
|
||||
return;
|
||||
}
|
||||
form.name = '';
|
||||
form.from = params.from;
|
||||
form.type = params.type;
|
||||
@@ -205,9 +209,9 @@ const acceptParams = async (params: DialogProps): Promise<void> => {
|
||||
random();
|
||||
loadOptions();
|
||||
await Promise.all([loadUsers(), loadGrants()]);
|
||||
if (users.value.length) {
|
||||
if (activeUsers.value.length) {
|
||||
form.userMode = 'select';
|
||||
const user = users.value[0];
|
||||
const user = activeUsers.value[0];
|
||||
form.userKey = `${user.username}@${user.host}`;
|
||||
syncUser();
|
||||
}
|
||||
@@ -254,7 +258,7 @@ const userDatabases = (user: Database.MysqlUser) => {
|
||||
};
|
||||
|
||||
const syncUser = () => {
|
||||
const item = users.value.find((item) => `${item.username}@${item.host}` === form.userKey);
|
||||
const item = activeUsers.value.find((item) => `${item.username}@${item.host}` === form.userKey);
|
||||
if (!item) return;
|
||||
form.username = item.username;
|
||||
form.host = item.host;
|
||||
@@ -268,7 +272,7 @@ const changeUserMode = () => {
|
||||
return;
|
||||
}
|
||||
if (form.userMode === 'select') {
|
||||
const user = users.value?.[0];
|
||||
const user = activeUsers.value?.[0];
|
||||
form.userKey = user ? `${user.username}@${user.host}` : '';
|
||||
syncUser();
|
||||
return;
|
||||
|
||||
@@ -137,47 +137,16 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('commons.login.username')" show-overflow-tooltip prop="username">
|
||||
<el-table-column :label="$t('database.authorizedUsers')" min-width="130">
|
||||
<template #default="{ row }">
|
||||
<div class="flex items-center" v-if="row.username">
|
||||
<span>
|
||||
{{ row.username }}
|
||||
</span>
|
||||
</div>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('commons.login.password')" prop="password">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.username === ''">-</span>
|
||||
<div class="flex items-center flex-wrap" v-if="row.password && row.username">
|
||||
<div class="star-center" v-if="!row.showPassword">
|
||||
<span>**********</span>
|
||||
</div>
|
||||
<div>
|
||||
<span v-if="row.showPassword">
|
||||
{{ row.password }}
|
||||
</span>
|
||||
</div>
|
||||
<el-button
|
||||
v-if="!row.showPassword"
|
||||
link
|
||||
@click="row.showPassword = true"
|
||||
icon="View"
|
||||
class="ml-1.5"
|
||||
></el-button>
|
||||
<el-button
|
||||
v-if="row.showPassword"
|
||||
link
|
||||
@click="row.showPassword = false"
|
||||
icon="Hide"
|
||||
class="ml-1.5"
|
||||
></el-button>
|
||||
<div>
|
||||
<CopyButton :content="row.password" />
|
||||
</div>
|
||||
</div>
|
||||
<span v-if="row.password === '' && row.username">-</span>
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
:disabled="row.isDelete"
|
||||
@click="openAuthorizationManagement(row)"
|
||||
>
|
||||
{{ $t('database.authorizedUserCount', [row.authorizedUsers?.length || 0]) }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('commons.table.description')" prop="description" show-overflow-tooltip>
|
||||
@@ -338,10 +307,13 @@ const mysqlVersion = ref();
|
||||
|
||||
const dialogRef = ref();
|
||||
const onOpenDialog = async () => {
|
||||
if (!currentDB.value?.database) {
|
||||
return;
|
||||
}
|
||||
let params = {
|
||||
from: currentDB.value.from,
|
||||
type: currentDB.value.type,
|
||||
database: currentDBName.value,
|
||||
database: currentDB.value.database,
|
||||
};
|
||||
dialogRef.value!.acceptParams(params);
|
||||
};
|
||||
@@ -354,10 +326,13 @@ const uploadRef = ref();
|
||||
|
||||
const connRef = ref();
|
||||
const onChangeConn = async () => {
|
||||
if (!currentDB.value?.database) {
|
||||
return;
|
||||
}
|
||||
connRef.value!.acceptParams({
|
||||
from: currentDB.value.from,
|
||||
type: currentDB.value.type,
|
||||
database: currentDBName.value,
|
||||
database: currentDB.value.database,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -395,7 +370,6 @@ const changeDatabase = async () => {
|
||||
appKey.value = item.type;
|
||||
appName.value = item.database;
|
||||
search();
|
||||
userRef.value?.loadContext();
|
||||
appStatusRef.value?.onCheck(appKey.value, appName.value);
|
||||
return;
|
||||
}
|
||||
@@ -409,7 +383,6 @@ const changeDatabase = async () => {
|
||||
}
|
||||
}
|
||||
search();
|
||||
userRef.value?.loadContext();
|
||||
};
|
||||
|
||||
const search = async (column?: any) => {
|
||||
@@ -417,6 +390,9 @@ const search = async (column?: any) => {
|
||||
};
|
||||
|
||||
const searchDatabases = async (column?: any) => {
|
||||
if (!currentDB.value?.database) {
|
||||
return;
|
||||
}
|
||||
const requestID = ++grantSummaryRequestID;
|
||||
paginationConfig.orderBy = column?.order ? column.prop : paginationConfig.orderBy;
|
||||
paginationConfig.order = column?.order ? column.order : paginationConfig.order;
|
||||
@@ -438,7 +414,7 @@ const searchDatabases = async (column?: any) => {
|
||||
};
|
||||
|
||||
const loadGrantSummary = async (items: Database.MysqlDBInfo[], requestID: number) => {
|
||||
if (!currentDB.value || items.length === 0) {
|
||||
if (!currentDB.value?.database || items.length === 0) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -451,10 +427,7 @@ const loadGrantSummary = async (items: Database.MysqlDBInfo[], requestID: number
|
||||
}
|
||||
for (const item of data.value) {
|
||||
const users = res.data?.[item.name] || [];
|
||||
const user = users.find((user) => user.password) || users[0];
|
||||
item.username = user?.username || '';
|
||||
item.password = user?.password || '';
|
||||
item.permission = user?.host || '';
|
||||
item.authorizedUsers = users;
|
||||
}
|
||||
} catch {
|
||||
return;
|
||||
@@ -462,9 +435,22 @@ const loadGrantSummary = async (items: Database.MysqlDBInfo[], requestID: number
|
||||
};
|
||||
|
||||
const openUserDrawer = async () => {
|
||||
if (!currentDB.value?.database) {
|
||||
return;
|
||||
}
|
||||
userRef.value!.acceptParams({ database: currentDB.value.database });
|
||||
};
|
||||
|
||||
const openAuthorizationManagement = (row: Database.MysqlDBInfo) => {
|
||||
if (!currentDB.value) {
|
||||
return;
|
||||
}
|
||||
bindRef.value!.acceptParams({
|
||||
database: currentDB.value.database,
|
||||
db: row.name,
|
||||
});
|
||||
};
|
||||
|
||||
const loadDB = async () => {
|
||||
ElMessageBox.confirm(i18n.global.t('database.loadFromRemoteHelper'), i18n.global.t('commons.msg.infoTitle'), {
|
||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||
@@ -609,19 +595,6 @@ const onDelete = async (row: Database.MysqlDBInfo) => {
|
||||
};
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
label: i18n.global.t('database.userBind'),
|
||||
permission: true,
|
||||
disabled: (row: Database.MysqlDBInfo) => {
|
||||
return row.isDelete;
|
||||
},
|
||||
click: (row: Database.MysqlDBInfo) => {
|
||||
bindRef.value!.acceptParams({
|
||||
database: currentDBName.value,
|
||||
db: row.name,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('database.backupList'),
|
||||
permission: true,
|
||||
@@ -664,7 +637,6 @@ const buttons = [
|
||||
|
||||
const onBindSearch = async () => {
|
||||
await search();
|
||||
userRef.value?.loadContext();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -6,15 +6,23 @@
|
||||
</el-button>
|
||||
</div>
|
||||
<ComplexTable :data="users" :heightDiff="260">
|
||||
<el-table-column
|
||||
:label="$t('commons.login.username')"
|
||||
prop="username"
|
||||
show-overflow-tooltip
|
||||
min-width="120"
|
||||
/>
|
||||
<el-table-column :label="$t('commons.login.password')" prop="password" min-width="140">
|
||||
<el-table-column :label="$t('commons.login.username')" prop="username" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<span v-if="!row.password">-</span>
|
||||
<span>{{ row.username }}@{{ row.host }}</span>
|
||||
<el-tag v-if="row.isDelete" round type="info" class="ml-1" size="small">
|
||||
{{ $t('database.isDelete') }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('commons.login.password')" prop="password" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.isDelete">-</span>
|
||||
<div v-else-if="!row.password" class="password-cell">
|
||||
<el-tag type="warning" size="small">{{ $t('database.passwordPendingSupplement') }}</el-tag>
|
||||
<el-button link type="primary" @click="openSupplementPasswordDialog(row)">
|
||||
{{ $t('database.supplementPassword') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="password-cell" v-else>
|
||||
<span v-if="!row.showPassword" class="password-text">**********</span>
|
||||
<span v-else class="password-text">{{ row.password }}</span>
|
||||
@@ -91,7 +99,7 @@
|
||||
<el-input type="textarea" clearable v-model="userForm.description" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="userDialogMode === 'edit'" :label="$t('menu.database')" prop="dbs">
|
||||
<el-select v-model="userForm.dbs" filterable multiple collapse-tags collapse-tags-tooltip>
|
||||
<el-select v-model="userForm.dbs" filterable multiple collapse-tags-tooltip>
|
||||
<el-option v-for="item in databases" :key="item.name" :label="item.name" :value="item.name" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
@@ -106,6 +114,32 @@
|
||||
</template>
|
||||
</DialogPro>
|
||||
|
||||
<DialogPro v-model="supplementDialogVisible" :title="$t('database.supplementPassword')" size="small">
|
||||
<el-form
|
||||
ref="supplementFormRef"
|
||||
:model="supplementForm"
|
||||
:rules="supplementRules"
|
||||
label-position="top"
|
||||
v-loading="loading"
|
||||
>
|
||||
<el-form-item :label="$t('commons.login.username')">
|
||||
<el-input :model-value="`${supplementForm.username}@${supplementForm.host}`" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('commons.login.password')" prop="password">
|
||||
<el-input type="password" clearable show-password v-model.trim="supplementForm.password" />
|
||||
<span class="input-help">{{ $t('database.supplementPasswordHelper') }}</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="supplementDialogVisible = false" :disabled="loading">
|
||||
{{ $t('commons.button.cancel') }}
|
||||
</el-button>
|
||||
<el-button type="primary" @click="submitSupplementPassword" :disabled="loading">
|
||||
{{ $t('commons.button.confirm') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</DialogPro>
|
||||
|
||||
<DialogPro v-model="deleteDialogVisible" :title="$t('commons.button.delete')" size="small">
|
||||
<div v-loading="loading">
|
||||
<div class="delete-user-title">
|
||||
@@ -124,9 +158,16 @@
|
||||
</div>
|
||||
<div class="delete-user-section">
|
||||
<div>
|
||||
<span style="font-size: 12px">{{ $t('database.delete') }}</span>
|
||||
<span style="font-size: 12px; color: red; font-weight: 500">{{ deleteUser.username }}</span>
|
||||
<span style="font-size: 12px">{{ $t('database.deleteUserHelper') }}</span>
|
||||
<template v-if="deleteUser.isDelete">
|
||||
<span style="font-size: 12px">{{ $t('database.deleteUserRecordHelper') }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span style="font-size: 12px">{{ $t('database.delete') }}</span>
|
||||
<span style="font-size: 12px; color: red; font-weight: 500">
|
||||
{{ deleteUser.username }}
|
||||
</span>
|
||||
<span style="font-size: 12px">{{ $t('database.deleteUserHelper') }}</span>
|
||||
</template>
|
||||
</div>
|
||||
<el-input v-model="deleteConfirmInput" :placeholder="deleteUser.username" />
|
||||
</div>
|
||||
@@ -160,6 +201,7 @@ import {
|
||||
searchMysqlDBs,
|
||||
searchMysqlGrants,
|
||||
searchMysqlUsers,
|
||||
saveMysqlUserPassword,
|
||||
updateMysqlUser,
|
||||
updateMysqlUserPassword,
|
||||
} from '@/api/modules/database';
|
||||
@@ -195,8 +237,19 @@ const deleteConfirmInput = ref('');
|
||||
const deleteUser = reactive({
|
||||
username: '',
|
||||
host: '',
|
||||
isDelete: false,
|
||||
});
|
||||
const deleteUserDbs = ref<string[]>([]);
|
||||
const supplementDialogVisible = ref(false);
|
||||
const supplementFormRef = ref();
|
||||
const supplementForm = reactive({
|
||||
username: '',
|
||||
host: '',
|
||||
password: '',
|
||||
});
|
||||
const supplementRules = reactive({
|
||||
password: [Rules.requiredInput, Rules.noSpace, Rules.illegal],
|
||||
});
|
||||
const checkPassword = (_rule: unknown, value: string, callback: (error?: Error) => void) => {
|
||||
if (!value && userDialogMode.value === 'edit') {
|
||||
callback();
|
||||
@@ -228,6 +281,9 @@ interface DialogProps {
|
||||
}
|
||||
|
||||
const acceptParams = async (params: DialogProps) => {
|
||||
if (!params.database) {
|
||||
return;
|
||||
}
|
||||
database.value = params.database;
|
||||
drawerVisible.value = true;
|
||||
await loadContext();
|
||||
@@ -256,6 +312,9 @@ const loadDatabases = async () => {
|
||||
};
|
||||
|
||||
const loadContext = async () => {
|
||||
if (!drawerVisible.value || !database.value) {
|
||||
return;
|
||||
}
|
||||
await Promise.all([loadUsers(), loadGrants(), loadDatabases()]);
|
||||
};
|
||||
|
||||
@@ -282,11 +341,40 @@ const changePermission = () => {
|
||||
const openDeleteUserDialog = (row: Database.MysqlUser) => {
|
||||
deleteUser.username = row.username;
|
||||
deleteUser.host = row.host;
|
||||
deleteUser.isDelete = row.isDelete;
|
||||
deleteUserDbs.value = userDatabases(row);
|
||||
deleteConfirmInput.value = '';
|
||||
deleteDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const openSupplementPasswordDialog = (row: Database.MysqlUser) => {
|
||||
supplementForm.username = row.username;
|
||||
supplementForm.host = row.host;
|
||||
supplementForm.password = '';
|
||||
supplementDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const submitSupplementPassword = async () => {
|
||||
if (!supplementFormRef.value) return;
|
||||
await supplementFormRef.value.validate(async (valid: boolean) => {
|
||||
if (!valid) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
await saveMysqlUserPassword({
|
||||
database: database.value,
|
||||
username: supplementForm.username,
|
||||
host: supplementForm.host,
|
||||
password: supplementForm.password,
|
||||
});
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
supplementDialogVisible.value = false;
|
||||
await loadUsers();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const openUserDialog = () => {
|
||||
userDialogMode.value = 'create';
|
||||
userForm.username = '';
|
||||
@@ -400,6 +488,7 @@ const userButtons = [
|
||||
{
|
||||
label: i18n.global.t('commons.button.edit'),
|
||||
permission: true,
|
||||
disabled: (row: Database.MysqlUser) => row.isDelete,
|
||||
click: (row: Database.MysqlUser) => {
|
||||
openUserEditDialog(row);
|
||||
},
|
||||
@@ -433,7 +522,6 @@ const submitDeleteUser = async () => {
|
||||
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
loadContext,
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user