feat(database): improve MySQL user authorization (#13326)

This commit is contained in:
ssongliu
2026-07-21 19:08:09 +08:00
committed by GitHub
parent af12526a6c
commit c77ddcfebe
48 changed files with 1233 additions and 318 deletions
+29
View File
@@ -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
+1
View File
@@ -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 {
+9
View File
@@ -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"`
}
+7
View File
@@ -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
+14
View File
@@ -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)
}
}
+102
View File
@@ -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)
}
}
+7
View File
@@ -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)
+6 -5
View File
@@ -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()
+10 -4
View File
@@ -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
View File
@@ -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")
+6 -5
View File
@@ -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()
+9 -2
View File
@@ -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:
+11
View File
@@ -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",
+1
View File
@@ -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'
+1
View 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'
+1
View File
@@ -110,6 +110,7 @@ ErrAppLimit: 'محدودیت نصب برنامه رسیده است'
ErrNotInstall: 'برنامه نصب نشده است'
ErrPortInOtherApp: 'پورت {{ .port }} قبلاً توسط برنامه {{ .apps }} اشغال شده است!'
ErrDbUserNotValid: 'نام کاربری/رمز عبور پایگاه داده مطابقت ندارد'
ErrMysqlUserUsedByApps: 'این کاربر پایگاه داده توسط [{{ .detail }}] استفاده می‌شود و میزبان آن قابل تغییر نیست؛ همچنین نمی‌توان آن را حذف یا مجوزش را لغو کرد'
ErrUpdateBuWebsite: 'برنامه به‌روزرسانی شد، اما به‌روزرسانی پیکربندی وب‌سایت ناموفق بود'
Err1PanelNetworkFailed: 'ایجاد شبکه پیش‌فرض کانتینر ناموفق بود! {{ .detail }}'
ErrFileParse: 'تجزیه فایل docker-compose برنامه ناموفق بود'
+1
View File
@@ -110,6 +110,7 @@ ErrAppLimit: 'インストールされているアプリケーションの数が
ErrNotInstall: 'アプリケーションがインストールされていません'
ErrPortInOtherApp: '{{ .port }} ポートは既にアプリケーション {{ .apps }} によって使用されています'
ErrDbUserNotValid: '既存のデータベース、ユーザー名、およびパスワードが一致しません'
ErrMysqlUserUsedByApps: 'このデータベースユーザーは【{{ .detail }}】で使用されているため、ホストの変更、削除、または権限の取り消しはできません'
ErrUpdateBuWebsite: 'アプリケーションは正常に更新されましたが、Web サイト構成ファイルの変更に失敗しました。設定を確認してください'
Err1PanelNetworkFailed: 'デフォルトのコンテナ ネットワークの作成に失敗しました。 {{ .detail }}'
ErrFileParse: 'アプリケーションの docker-compose ファイルの解析に失敗しました'
+1
View File
@@ -110,6 +110,7 @@ ErrAppLimit: '설치된 애플리케이션 수가 한도를 초과했습니다'
ErrNotInstall: '응용 프로그램이 설치되지 않았습니다'
ErrPortInOtherApp: '{{ .port }} 포트는 이미 {{ .apps }} 애플리케이션에 의해 사용되고 있습니다'
ErrDbUserNotValid: '기존 데이터베이스, 사용자 이름 및 비밀번호가 일치하지 않습니다'
ErrMysqlUserUsedByApps: '이 데이터베이스 사용자는 [{{ .detail }}]에서 사용 중이므로 호스트 변경, 삭제 또는 권한 해제를 할 수 없습니다'
ErrUpdateBuWebsite: '응용 프로그램이 성공적으로 업데이트되었지만, 웹사이트 구성 파일 수정에 실패했습니다. 구성을 확인하세요'
Err1PanelNetworkFailed: '기본 컨테이너 네트워크 생성에 실패했습니다 {{ .세부 사항 }}'
ErrFileParse: '응용 프로그램 docker-compose 파일 구문 분석에 실패했습니다'
+1
View File
@@ -95,6 +95,7 @@ ErrAppLimit: 'ຮອດຂີດຈຳກັດການຕິດຕັ້ງແ
ErrNotInstall: 'ແອັບພລິເຄຊັນຍັງບໍ່ໄດ້ຕິດຕັ້ງ'
ErrPortInOtherApp: 'ພອດ {{ .port }} ຖືກໃຊ້ງານແລ້ວໂດຍແອັບພລິເຄຊັນ {{ .apps }}!'
ErrDbUserNotValid: 'ຊື່ຜູ້ໃຊ້ ຫຼື ລະຫັດຜ່ານຖານຂໍ້ມູນບໍ່ຖືກຕ້ອງ'
ErrMysqlUserUsedByApps: 'ຜູ້ໃຊ້ຖານຂໍ້ມູນນີ້ກຳລັງຖືກໃຊ້ໂດຍ [{{ .detail }}] ແລະບໍ່ສາມາດປ່ຽນ Host, ລຶບ ຫຼືຖອນການອະນຸຍາດໄດ້'
ErrUpdateBuWebsite: 'ອັບເດດແອັບແລ້ວ, ແຕ່ການອັບເດດການຕັ້ງຄ່າເວັບໄຊລົ້ມເຫຼວ'
Err1PanelNetworkFailed: 'ການສ້າງເຄືອຂ່າຍຄອນເທນເນີເລີ່ມຕົ້ນລົ້ມເຫຼວ! {{ .detail }}'
ErrFileParse: 'ການວິເຄາະໄຟລ໌ docker-compose ຂອງແອັບລົ້ມເຫຼວ'
+1
View File
@@ -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'
+1
View File
@@ -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'
+1
View File
@@ -110,6 +110,7 @@ ErrAppLimit: 'Количество установленных приложени
ErrNotInstall: 'Приложение не установлено'
ErrPortInOtherApp: 'Порт {{ .port }} уже занят приложением {{ .apps }}'
ErrDbUserNotValid: 'Существующая база данных, имя пользователя и пароль не совпадают'
ErrMysqlUserUsedByApps: 'Этот пользователь базы данных используется приложениями [{{ .detail }}]; его хост нельзя изменить, а самого пользователя нельзя удалить или лишить прав'
ErrUpdateBuWebsite: 'Приложение успешно обновлено, но изменение файла конфигурации веб-сайта не удалось. Пожалуйста, проверьте конфигурацию'
Err1PanelNetworkFailed: 'Создание сети контейнеров по умолчанию не удалось {{ .detail }}'
ErrFileParse: 'Ошибка анализа файла docker-compose приложения'
+1
View File
@@ -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'
+1
View File
@@ -110,6 +110,7 @@ ErrAppLimit: '應用程式超出安裝數量限制'
ErrNotInstall: '應用程式未安裝'
ErrPortInOtherApp: '{{ .port }} 連接埠已被應用程式{{ .apps }} 佔用!'
ErrDbUserNotValid: '存量資料庫,使用者名稱密碼不符!'
ErrMysqlUserUsedByApps: '該資料庫使用者已被【{{ .detail }}】應用使用,無法修改 Host、刪除或撤銷授權'
ErrUpdateBuWebsite: '應用程式更新成功,但網站設定檔修改失敗,請檢查設定!'
Err1PanelNetworkFailed: '預設容器網路建立失敗! {{ .detail }}'
ErrFileParse: '應用docker-compose 檔解析失敗!'
+1
View File
@@ -110,6 +110,7 @@ ErrAppLimit: "应用安装数量超限"
ErrNotInstall: "应用未安装"
ErrPortInOtherApp: "{{ .port }} 端口已被应用 {{ .apps }} 占用!"
ErrDbUserNotValid: "存量数据库用户名或密码不匹配"
ErrMysqlUserUsedByApps: "该数据库用户已被【{{ .detail }}】应用使用,无法修改 Host、删除或者撤销授权"
ErrUpdateBuWebsite: '应用更新成功,但网站配置文件修改失败,请检查配置'
Err1PanelNetworkFailed: '默认容器网络创建失败!{{ .detail }}'
ErrFileParse: '应用 docker-compose 解析失败'
+39 -39
View File
@@ -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
},
}
+1
View File
@@ -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)
+54 -1
View File
@@ -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"
},
+54 -1
View File
@@ -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"
},
+11
View File
@@ -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",
+3 -1
View File
@@ -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 {
+5
View File
@@ -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);
};
+15 -2
View File
@@ -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',
+16 -2
View File
@@ -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',
+15 -2
View File
@@ -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: 'سرور از راه دور | سرورهای از راه دور',
+15 -2
View File
@@ -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: 'リモートサーバー|リモートサーバー',
+15 -2
View File
@@ -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: '원격 서버 | 원격 서버들',
+15 -2
View File
@@ -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: 'ເຊີເວີທາງໄກ | ເຊີເວີທາງໄກ',
+16 -2
View File
@@ -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',
+16 -2
View File
@@ -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 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 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',
+15 -2
View File
@@ -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: 'Удаленный сервер | Удаленные серверы',
+15 -2
View File
@@ -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',
+15 -2
View File
@@ -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: '遠端伺服器',
+15 -2
View File
@@ -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: '远程服务器',
+128 -26
View File
@@ -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;
+35 -63
View File
@@ -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(() => {
+101 -13
View File
@@ -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>