diff --git a/agent/app/api/v2/database_mysql.go b/agent/app/api/v2/database_mysql.go index 20027ed54..f467deee2 100644 --- a/agent/app/api/v2/database_mysql.go +++ b/agent/app/api/v2/database_mysql.go @@ -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 diff --git a/agent/app/dto/database.go b/agent/app/dto/database.go index 46abf4389..633c29272 100644 --- a/agent/app/dto/database.go +++ b/agent/app/dto/database.go @@ -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 { diff --git a/agent/app/model/database_user.go b/agent/app/model/database_user.go index aa9cc2e24..322c6f0e8 100644 --- a/agent/app/model/database_user.go +++ b/agent/app/model/database_user.go @@ -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"` +} diff --git a/agent/app/repo/app_install_resource.go b/agent/app/repo/app_install_resource.go index 2211a41e7..e8a5c72f4 100644 --- a/agent/app/repo/app_install_resource.go +++ b/agent/app/repo/app_install_resource.go @@ -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 diff --git a/agent/app/repo/database_user.go b/agent/app/repo/database_user.go index b7e81df16..794c3ea76 100644 --- a/agent/app/repo/database_user.go +++ b/agent/app/repo/database_user.go @@ -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) + } +} diff --git a/agent/app/repo/database_user_grant.go b/agent/app/repo/database_user_grant.go new file mode 100644 index 000000000..b166fadb8 --- /dev/null +++ b/agent/app/repo/database_user_grant.go @@ -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) + } +} diff --git a/agent/app/repo/website.go b/agent/app/repo/website.go index bb0a012ab..cc1e472cf 100644 --- a/agent/app/repo/website.go +++ b/agent/app/repo/website.go @@ -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) diff --git a/agent/app/service/app_utils.go b/agent/app/service/app_utils.go index 64175c15d..5e3b42fe0 100644 --- a/agent/app/service/app_utils.go +++ b/agent/app/service/app_utils.go @@ -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() diff --git a/agent/app/service/backup_app.go b/agent/app/service/backup_app.go index 8bd82f67c..33ca43134 100644 --- a/agent/app/service/backup_app.go +++ b/agent/app/service/backup_app.go @@ -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 diff --git a/agent/app/service/database_mysql.go b/agent/app/service/database_mysql.go index 8ccba2439..17d7f2a1d 100644 --- a/agent/app/service/database_mysql.go +++ b/agent/app/service/database_mysql.go @@ -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") diff --git a/agent/app/service/entry.go b/agent/app/service/entry.go index 4a6e06fa9..fb91e430b 100644 --- a/agent/app/service/entry.go +++ b/agent/app/service/entry.go @@ -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() diff --git a/agent/app/service/website.go b/agent/app/service/website.go index 0d373b42b..414a5b407 100644 --- a/agent/app/service/website.go +++ b/agent/app/service/website.go @@ -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: diff --git a/agent/cmd/server/docs/x-log.json b/agent/cmd/server/docs/x-log.json index 42b89d672..a6073d189 100644 --- a/agent/cmd/server/docs/x-log.json +++ b/agent/cmd/server/docs/x-log.json @@ -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", diff --git a/agent/i18n/lang/en.yaml b/agent/i18n/lang/en.yaml index 6951e0003..98920334b 100644 --- a/agent/i18n/lang/en.yaml +++ b/agent/i18n/lang/en.yaml @@ -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' diff --git a/agent/i18n/lang/es-ES.yaml b/agent/i18n/lang/es-ES.yaml index 11c4bcd58..93dc9e23c 100644 --- a/agent/i18n/lang/es-ES.yaml +++ b/agent/i18n/lang/es-ES.yaml @@ -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' diff --git a/agent/i18n/lang/fa.yaml b/agent/i18n/lang/fa.yaml index 2e8e84d0f..0cff05afa 100644 --- a/agent/i18n/lang/fa.yaml +++ b/agent/i18n/lang/fa.yaml @@ -110,6 +110,7 @@ ErrAppLimit: 'محدودیت نصب برنامه رسیده است' ErrNotInstall: 'برنامه نصب نشده است' ErrPortInOtherApp: 'پورت {{ .port }} قبلاً توسط برنامه {{ .apps }} اشغال شده است!' ErrDbUserNotValid: 'نام کاربری/رمز عبور پایگاه داده مطابقت ندارد' +ErrMysqlUserUsedByApps: 'این کاربر پایگاه داده توسط [{{ .detail }}] استفاده می‌شود و میزبان آن قابل تغییر نیست؛ همچنین نمی‌توان آن را حذف یا مجوزش را لغو کرد' ErrUpdateBuWebsite: 'برنامه به‌روزرسانی شد، اما به‌روزرسانی پیکربندی وب‌سایت ناموفق بود' Err1PanelNetworkFailed: 'ایجاد شبکه پیش‌فرض کانتینر ناموفق بود! {{ .detail }}' ErrFileParse: 'تجزیه فایل docker-compose برنامه ناموفق بود' diff --git a/agent/i18n/lang/ja.yaml b/agent/i18n/lang/ja.yaml index e10e42599..120a5e7c1 100644 --- a/agent/i18n/lang/ja.yaml +++ b/agent/i18n/lang/ja.yaml @@ -110,6 +110,7 @@ ErrAppLimit: 'インストールされているアプリケーションの数が ErrNotInstall: 'アプリケーションがインストールされていません' ErrPortInOtherApp: '{{ .port }} ポートは既にアプリケーション {{ .apps }} によって使用されています' ErrDbUserNotValid: '既存のデータベース、ユーザー名、およびパスワードが一致しません' +ErrMysqlUserUsedByApps: 'このデータベースユーザーは【{{ .detail }}】で使用されているため、ホストの変更、削除、または権限の取り消しはできません' ErrUpdateBuWebsite: 'アプリケーションは正常に更新されましたが、Web サイト構成ファイルの変更に失敗しました。設定を確認してください' Err1PanelNetworkFailed: 'デフォルトのコンテナ ネットワークの作成に失敗しました。 {{ .detail }}' ErrFileParse: 'アプリケーションの docker-compose ファイルの解析に失敗しました' diff --git a/agent/i18n/lang/ko.yaml b/agent/i18n/lang/ko.yaml index cd8803abb..f3f0a3de9 100644 --- a/agent/i18n/lang/ko.yaml +++ b/agent/i18n/lang/ko.yaml @@ -110,6 +110,7 @@ ErrAppLimit: '설치된 애플리케이션 수가 한도를 초과했습니다' ErrNotInstall: '응용 프로그램이 설치되지 않았습니다' ErrPortInOtherApp: '{{ .port }} 포트는 이미 {{ .apps }} 애플리케이션에 의해 사용되고 있습니다' ErrDbUserNotValid: '기존 데이터베이스, 사용자 이름 및 비밀번호가 일치하지 않습니다' +ErrMysqlUserUsedByApps: '이 데이터베이스 사용자는 [{{ .detail }}]에서 사용 중이므로 호스트 변경, 삭제 또는 권한 해제를 할 수 없습니다' ErrUpdateBuWebsite: '응용 프로그램이 성공적으로 업데이트되었지만, 웹사이트 구성 파일 수정에 실패했습니다. 구성을 확인하세요' Err1PanelNetworkFailed: '기본 컨테이너 네트워크 생성에 실패했습니다 {{ .세부 사항 }}' ErrFileParse: '응용 프로그램 docker-compose 파일 구문 분석에 실패했습니다' diff --git a/agent/i18n/lang/lo.yaml b/agent/i18n/lang/lo.yaml index 3c064e2d7..74bef180e 100644 --- a/agent/i18n/lang/lo.yaml +++ b/agent/i18n/lang/lo.yaml @@ -95,6 +95,7 @@ ErrAppLimit: 'ຮອດຂີດຈຳກັດການຕິດຕັ້ງແ ErrNotInstall: 'ແອັບພລິເຄຊັນຍັງບໍ່ໄດ້ຕິດຕັ້ງ' ErrPortInOtherApp: 'ພອດ {{ .port }} ຖືກໃຊ້ງານແລ້ວໂດຍແອັບພລິເຄຊັນ {{ .apps }}!' ErrDbUserNotValid: 'ຊື່ຜູ້ໃຊ້ ຫຼື ລະຫັດຜ່ານຖານຂໍ້ມູນບໍ່ຖືກຕ້ອງ' +ErrMysqlUserUsedByApps: 'ຜູ້ໃຊ້ຖານຂໍ້ມູນນີ້ກຳລັງຖືກໃຊ້ໂດຍ [{{ .detail }}] ແລະບໍ່ສາມາດປ່ຽນ Host, ລຶບ ຫຼືຖອນການອະນຸຍາດໄດ້' ErrUpdateBuWebsite: 'ອັບເດດແອັບແລ້ວ, ແຕ່ການອັບເດດການຕັ້ງຄ່າເວັບໄຊລົ້ມເຫຼວ' Err1PanelNetworkFailed: 'ການສ້າງເຄືອຂ່າຍຄອນເທນເນີເລີ່ມຕົ້ນລົ້ມເຫຼວ! {{ .detail }}' ErrFileParse: 'ການວິເຄາະໄຟລ໌ docker-compose ຂອງແອັບລົ້ມເຫຼວ' diff --git a/agent/i18n/lang/ms.yaml b/agent/i18n/lang/ms.yaml index 9cbdd1d0a..c09e8ec0b 100644 --- a/agent/i18n/lang/ms.yaml +++ b/agent/i18n/lang/ms.yaml @@ -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' diff --git a/agent/i18n/lang/pt-BR.yaml b/agent/i18n/lang/pt-BR.yaml index 8d30bbc3d..ac517c3ae 100644 --- a/agent/i18n/lang/pt-BR.yaml +++ b/agent/i18n/lang/pt-BR.yaml @@ -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' diff --git a/agent/i18n/lang/ru.yaml b/agent/i18n/lang/ru.yaml index 6e03ddc9d..eea4c51eb 100644 --- a/agent/i18n/lang/ru.yaml +++ b/agent/i18n/lang/ru.yaml @@ -110,6 +110,7 @@ ErrAppLimit: 'Количество установленных приложени ErrNotInstall: 'Приложение не установлено' ErrPortInOtherApp: 'Порт {{ .port }} уже занят приложением {{ .apps }}' ErrDbUserNotValid: 'Существующая база данных, имя пользователя и пароль не совпадают' +ErrMysqlUserUsedByApps: 'Этот пользователь базы данных используется приложениями [{{ .detail }}]; его хост нельзя изменить, а самого пользователя нельзя удалить или лишить прав' ErrUpdateBuWebsite: 'Приложение успешно обновлено, но изменение файла конфигурации веб-сайта не удалось. Пожалуйста, проверьте конфигурацию' Err1PanelNetworkFailed: 'Создание сети контейнеров по умолчанию не удалось {{ .detail }}' ErrFileParse: 'Ошибка анализа файла docker-compose приложения' diff --git a/agent/i18n/lang/tr.yaml b/agent/i18n/lang/tr.yaml index 40ad415c2..1a7c6b73b 100644 --- a/agent/i18n/lang/tr.yaml +++ b/agent/i18n/lang/tr.yaml @@ -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' diff --git a/agent/i18n/lang/zh-Hant.yaml b/agent/i18n/lang/zh-Hant.yaml index efdeddddc..b8c201609 100644 --- a/agent/i18n/lang/zh-Hant.yaml +++ b/agent/i18n/lang/zh-Hant.yaml @@ -110,6 +110,7 @@ ErrAppLimit: '應用程式超出安裝數量限制' ErrNotInstall: '應用程式未安裝' ErrPortInOtherApp: '{{ .port }} 連接埠已被應用程式{{ .apps }} 佔用!' ErrDbUserNotValid: '存量資料庫,使用者名稱密碼不符!' +ErrMysqlUserUsedByApps: '該資料庫使用者已被【{{ .detail }}】應用使用,無法修改 Host、刪除或撤銷授權' ErrUpdateBuWebsite: '應用程式更新成功,但網站設定檔修改失敗,請檢查設定!' Err1PanelNetworkFailed: '預設容器網路建立失敗! {{ .detail }}' ErrFileParse: '應用docker-compose 檔解析失敗!' diff --git a/agent/i18n/lang/zh.yaml b/agent/i18n/lang/zh.yaml index d3cdcdb1b..936c75e88 100644 --- a/agent/i18n/lang/zh.yaml +++ b/agent/i18n/lang/zh.yaml @@ -110,6 +110,7 @@ ErrAppLimit: "应用安装数量超限" ErrNotInstall: "应用未安装" ErrPortInOtherApp: "{{ .port }} 端口已被应用 {{ .apps }} 占用!" ErrDbUserNotValid: "存量数据库用户名或密码不匹配" +ErrMysqlUserUsedByApps: "该数据库用户已被【{{ .detail }}】应用使用,无法修改 Host、删除或者撤销授权" ErrUpdateBuWebsite: '应用更新成功,但网站配置文件修改失败,请检查配置' Err1PanelNetworkFailed: '默认容器网络创建失败!{{ .detail }}' ErrFileParse: '应用 docker-compose 解析失败' diff --git a/agent/init/migration/migrations/init.go b/agent/init/migration/migrations/init.go index 600cfae84..f31bda168 100644 --- a/agent/init/migration/migrations/init.go +++ b/agent/init/migration/migrations/init.go @@ -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 }, } diff --git a/agent/router/ro_database.go b/agent/router/ro_database.go index 2c2635c27..95f57223b 100644 --- a/agent/router/ro_database.go +++ b/agent/router/ro_database.go @@ -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) diff --git a/core/cmd/server/docs/docs.go b/core/cmd/server/docs/docs.go index b75900c20..62d722d4b 100644 --- a/core/cmd/server/docs/docs.go +++ b/core/cmd/server/docs/docs.go @@ -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" }, diff --git a/core/cmd/server/docs/swagger.json b/core/cmd/server/docs/swagger.json index 11110e45d..4f798c1d3 100644 --- a/core/cmd/server/docs/swagger.json +++ b/core/cmd/server/docs/swagger.json @@ -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" }, diff --git a/core/cmd/server/docs/x-log.json b/core/cmd/server/docs/x-log.json index 42b89d672..a6073d189 100644 --- a/core/cmd/server/docs/x-log.json +++ b/core/cmd/server/docs/x-log.json @@ -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", diff --git a/frontend/src/api/interface/database.ts b/frontend/src/api/interface/database.ts index d4eb2a021..7ab77c4a2 100644 --- a/frontend/src/api/interface/database.ts +++ b/frontend/src/api/interface/database.ts @@ -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 { diff --git a/frontend/src/api/modules/database.ts b/frontend/src/api/modules/database.ts index a27bb4aaa..08910a100 100644 --- a/frontend/src/api/modules/database.ts +++ b/frontend/src/api/modules/database.ts @@ -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(`/databases/grants/search`, params); }; diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index ae6cfd285..c93a0d99c 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -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', diff --git a/frontend/src/lang/modules/es-es.ts b/frontend/src/lang/modules/es-es.ts index a137af8dd..56706cc17 100644 --- a/frontend/src/lang/modules/es-es.ts +++ b/frontend/src/lang/modules/es-es.ts @@ -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', diff --git a/frontend/src/lang/modules/fa.ts b/frontend/src/lang/modules/fa.ts index 8d82478c5..e045cf25f 100644 --- a/frontend/src/lang/modules/fa.ts +++ b/frontend/src/lang/modules/fa.ts @@ -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: 'سرور از راه دور | سرورهای از راه دور', diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts index 91c643b20..ca5400ac6 100644 --- a/frontend/src/lang/modules/ja.ts +++ b/frontend/src/lang/modules/ja.ts @@ -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: 'リモートサーバー|リモートサーバー', diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts index 45aef8836..154747086 100644 --- a/frontend/src/lang/modules/ko.ts +++ b/frontend/src/lang/modules/ko.ts @@ -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: '원격 서버 | 원격 서버들', diff --git a/frontend/src/lang/modules/lo.ts b/frontend/src/lang/modules/lo.ts index 1c0811c46..f64032736 100644 --- a/frontend/src/lang/modules/lo.ts +++ b/frontend/src/lang/modules/lo.ts @@ -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: 'ເຊີເວີທາງໄກ | ເຊີເວີທາງໄກ', diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts index a78997fe3..750ee53fd 100644 --- a/frontend/src/lang/modules/ms.ts +++ b/frontend/src/lang/modules/ms.ts @@ -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', diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts index ea7ec09d0..4d75f12f3 100644 --- a/frontend/src/lang/modules/pt-br.ts +++ b/frontend/src/lang/modules/pt-br.ts @@ -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', diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts index ca990b14e..24527387a 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -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: 'Удаленный сервер | Удаленные серверы', diff --git a/frontend/src/lang/modules/tr.ts b/frontend/src/lang/modules/tr.ts index a168ea4ca..6d83de0c9 100644 --- a/frontend/src/lang/modules/tr.ts +++ b/frontend/src/lang/modules/tr.ts @@ -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', diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts index 8c54e916b..4e76437d0 100644 --- a/frontend/src/lang/modules/zh-Hant.ts +++ b/frontend/src/lang/modules/zh-Hant.ts @@ -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: '遠端伺服器', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 24262c904..53a619254 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -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: '远程服务器', diff --git a/frontend/src/views/database/mysql/bind/index.vue b/frontend/src/views/database/mysql/bind/index.vue index 970820ac6..1a27050cc 100644 --- a/frontend/src/views/database/mysql/bind/index.vue +++ b/frontend/src/views/database/mysql/bind/index.vue @@ -1,5 +1,41 @@ @@ -93,16 +129,24 @@