From 0df2a9107dbb6b59897f71d505c52f4b260aeb21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=97=E5=AD=90?= Date: Mon, 25 Nov 2024 03:01:02 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=95=B0=E6=8D=AE=E5=BA=93=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/biz/database.go | 18 +- internal/biz/database_server.go | 31 ++-- internal/biz/database_user.go | 26 ++- internal/data/database.go | 22 +-- internal/data/database_server.go | 169 +++++++++++++----- internal/data/database_user.go | 213 +++++++++++++++++++++++ internal/data/init.go | 2 +- internal/http/request/database_server.go | 1 - internal/http/request/database_user.go | 3 +- internal/route/http.go | 19 +- internal/service/database.go | 4 +- internal/service/database_server.go | 19 +- internal/service/database_user.go | 85 +++++++++ pkg/db/mysql.go | 29 ++- pkg/db/postgres.go | 87 ++++----- pkg/types/database.go | 15 -- web/src/api/panel/database/index.ts | 10 +- 17 files changed, 593 insertions(+), 160 deletions(-) create mode 100644 internal/data/database_user.go create mode 100644 internal/service/database_user.go delete mode 100644 pkg/types/database.go diff --git a/internal/biz/database.go b/internal/biz/database.go index a3c9c222..f5e97052 100644 --- a/internal/biz/database.go +++ b/internal/biz/database.go @@ -2,12 +2,24 @@ package biz import ( "github.com/TheTNB/panel/internal/http/request" - "github.com/TheTNB/panel/pkg/types" ) +type DatabaseStatus string + +const ( + DatabaseStatusValid DatabaseStatus = "valid" + DatabaseStatusInvalid DatabaseStatus = "invalid" +) + +type Database struct { + Name string `json:"name"` + ServerID uint `json:"server_id"` + Status DatabaseStatus `json:"status"` + Remark string `json:"remark"` +} + type DatabaseRepo interface { - Count() (int64, error) - List(page, limit uint) ([]types.Database, int64, error) + List(page, limit uint) ([]*Database, int64, error) Create(req *request.DatabaseCreate) error Delete(serverID uint, name string) error } diff --git a/internal/biz/database_server.go b/internal/biz/database_server.go index f06d01f4..6a59eb1e 100644 --- a/internal/biz/database_server.go +++ b/internal/biz/database_server.go @@ -14,20 +14,30 @@ type DatabaseType string const ( DatabaseTypeMysql DatabaseType = "mysql" DatabaseTypePostgresql DatabaseType = "postgresql" + DatabaseTypeMongoDB DatabaseType = "mongodb" + DatabaseSQLite DatabaseType = "sqlite" DatabaseTypeRedis DatabaseType = "redis" ) +type DatabaseServerStatus string + +const ( + DatabaseServerStatusValid DatabaseServerStatus = "valid" + DatabaseServerStatusInvalid DatabaseServerStatus = "invalid" +) + type DatabaseServer struct { - ID uint `gorm:"primaryKey" json:"id"` - Name string `gorm:"not null;unique" json:"name"` - Type DatabaseType `gorm:"not null" json:"type"` - Host string `gorm:"not null" json:"host"` - Port uint `gorm:"not null" json:"port"` - Username string `gorm:"not null" json:"username"` - Password string `gorm:"not null" json:"password"` - Remark string `gorm:"not null" json:"remark"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uint `gorm:"primaryKey" json:"id"` + Name string `gorm:"not null;unique" json:"name"` + Type DatabaseType `gorm:"not null" json:"type"` + Host string `gorm:"not null" json:"host"` + Port uint `gorm:"not null" json:"port"` + Username string `gorm:"not null" json:"username"` + Password string `gorm:"not null" json:"password"` + Status DatabaseServerStatus `gorm:"-:all" json:"status"` + Remark string `gorm:"not null" json:"remark"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } func (r *DatabaseServer) BeforeSave(tx *gorm.DB) error { @@ -57,4 +67,5 @@ type DatabaseServerRepo interface { Create(req *request.DatabaseServerCreate) error Update(req *request.DatabaseServerUpdate) error Delete(id uint) error + Sync(id uint) error } diff --git a/internal/biz/database_user.go b/internal/biz/database_user.go index 6dbc72a4..ef6451a8 100644 --- a/internal/biz/database_user.go +++ b/internal/biz/database_user.go @@ -9,14 +9,24 @@ import ( "github.com/TheTNB/panel/internal/http/request" ) +type DatabaseUserStatus string + +const ( + DatabaseUserStatusValid DatabaseUserStatus = "valid" + DatabaseUserStatusInvalid DatabaseUserStatus = "invalid" +) + type DatabaseUser struct { - ID uint `gorm:"primaryKey" json:"id"` - ServerID uint `gorm:"not null" json:"server_id"` - Username string `gorm:"not null" json:"username"` - Password string `gorm:"not null" json:"password"` - Remark string `gorm:"not null" json:"remark"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uint `gorm:"primaryKey" json:"id"` + ServerID uint `gorm:"not null" json:"server_id"` + Username string `gorm:"not null" json:"username"` + Password string `gorm:"not null" json:"password"` + Host string `gorm:"not null" json:"host"` // 仅 mysql + Status DatabaseUserStatus `gorm:"-:all" json:"status"` // 仅显示 + Privileges map[string][]string `gorm:"-:all" json:"privileges"` // 仅显示 + Remark string `gorm:"not null" json:"remark"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } func (r *DatabaseUser) BeforeSave(tx *gorm.DB) error { @@ -46,5 +56,5 @@ type DatabaseUserRepo interface { Create(req *request.DatabaseUserCreate) error Update(req *request.DatabaseUserUpdate) error Delete(id uint) error - Sync(id uint) error + DeleteByServerID(serverID uint) error } diff --git a/internal/data/database.go b/internal/data/database.go index eeb4c654..d5b1bca9 100644 --- a/internal/data/database.go +++ b/internal/data/database.go @@ -9,7 +9,6 @@ import ( "github.com/TheTNB/panel/internal/biz" "github.com/TheTNB/panel/internal/http/request" "github.com/TheTNB/panel/pkg/db" - "github.com/TheTNB/panel/pkg/types" ) type databaseRepo struct{} @@ -18,22 +17,13 @@ func NewDatabaseRepo() biz.DatabaseRepo { return do.MustInvoke[biz.DatabaseRepo](injector) } -func (r databaseRepo) Count() (int64, error) { - var count int64 - if err := app.Orm.Model(&types.Database{}).Count(&count).Error; err != nil { - return 0, err - } - - return count, nil -} - -func (r databaseRepo) List(page, limit uint) ([]types.Database, int64, error) { +func (r databaseRepo) List(page, limit uint) ([]*biz.Database, int64, error) { var databaseServer []*biz.DatabaseServer if err := app.Orm.Model(&biz.DatabaseServer{}).Order("id desc").Find(&databaseServer).Error; err != nil { return nil, 0, err } - database := make([]types.Database, 0) + database := make([]*biz.Database, 0) for _, server := range databaseServer { switch server.Type { case biz.DatabaseTypeMysql: @@ -41,10 +31,10 @@ func (r databaseRepo) List(page, limit uint) ([]types.Database, int64, error) { if err == nil { if databases, err := mysql.Databases(); err == nil { for _, name := range databases { - database = append(database, types.Database{ + database = append(database, &biz.Database{ Name: name, ServerID: server.ID, - Status: types.DatabaseStatusValid, + Status: biz.DatabaseStatusValid, }) } } @@ -54,10 +44,10 @@ func (r databaseRepo) List(page, limit uint) ([]types.Database, int64, error) { if err == nil { if databases, err := postgres.Databases(); err == nil { for _, item := range databases { - database = append(database, types.Database{ + database = append(database, &biz.Database{ Name: item.Name, ServerID: server.ID, - Status: types.DatabaseStatusValid, + Status: biz.DatabaseStatusValid, }) } } diff --git a/internal/data/database_server.go b/internal/data/database_server.go index b729e52f..bf7922d6 100644 --- a/internal/data/database_server.go +++ b/internal/data/database_server.go @@ -2,6 +2,7 @@ package data import ( "fmt" + "slices" "github.com/samber/do/v2" @@ -30,6 +31,11 @@ func (r databaseServerRepo) List(page, limit uint) ([]*biz.DatabaseServer, int64 var databaseServer []*biz.DatabaseServer var total int64 err := app.Orm.Model(&biz.DatabaseServer{}).Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&databaseServer).Error + + for server := range slices.Values(databaseServer) { + r.checkServer(server) + } + return databaseServer, total, err } @@ -39,26 +45,12 @@ func (r databaseServerRepo) Get(id uint) (*biz.DatabaseServer, error) { return nil, err } + r.checkServer(databaseServer) + return databaseServer, nil } func (r databaseServerRepo) Create(req *request.DatabaseServerCreate) error { - switch biz.DatabaseType(req.Type) { - case biz.DatabaseTypeMysql: - if _, err := db.NewMySQL(req.Username, req.Password, fmt.Sprintf("%s:%d", req.Host, req.Port)); err != nil { - return err - } - case biz.DatabaseTypePostgresql: - if _, err := db.NewPostgres(req.Username, req.Password, req.Host, req.Port); err != nil { - return err - } - case biz.DatabaseTypeRedis: - if _, err := db.NewRedis(req.Username, req.Password, fmt.Sprintf("%s:%d", req.Host, req.Port)); err != nil { - return err - } - - } - databaseServer := &biz.DatabaseServer{ Name: req.Name, Type: biz.DatabaseType(req.Type), @@ -69,42 +61,125 @@ func (r databaseServerRepo) Create(req *request.DatabaseServerCreate) error { Remark: req.Remark, } + if !r.checkServer(databaseServer) { + return fmt.Errorf("check server connection failed") + } + return app.Orm.Create(databaseServer).Error } func (r databaseServerRepo) Update(req *request.DatabaseServerUpdate) error { - switch biz.DatabaseType(req.Type) { - case biz.DatabaseTypeMysql: - if _, err := db.NewMySQL(req.Username, req.Password, fmt.Sprintf("%s:%d", req.Host, req.Port)); err != nil { - return err - } - case biz.DatabaseTypePostgresql: - if _, err := db.NewPostgres(req.Username, req.Password, req.Host, req.Port); err != nil { - return err - } - case biz.DatabaseTypeRedis: - if _, err := db.NewRedis(req.Username, req.Password, fmt.Sprintf("%s:%d", req.Host, req.Port)); err != nil { - return err - } - - } - - return app.Orm.Model(&biz.DatabaseServer{}).Where("id = ?", req.ID).Select("*").Updates(&biz.DatabaseServer{ - Name: req.Name, - Type: biz.DatabaseType(req.Type), - Host: req.Host, - Port: req.Port, - Username: req.Username, - Password: req.Password, - Remark: req.Remark, - }).Error -} - -func (r databaseServerRepo) Delete(id uint) error { - ds := new(biz.DatabaseServer) - if err := app.Orm.Where("id = ?", id).First(ds).Error; err != nil { + server, err := r.Get(req.ID) + if err != nil { return err } - return app.Orm.Delete(&biz.DatabaseServer{}, id).Error + server.Name = req.Name + server.Host = req.Host + server.Port = req.Port + server.Username = req.Username + server.Password = req.Password + server.Remark = req.Remark + + if !r.checkServer(server) { + return fmt.Errorf("check server connection failed") + } + + return app.Orm.Save(server).Error +} + +func (r databaseServerRepo) Delete(id uint) error { + // 删除服务器下的所有用户 + if err := NewDatabaseUserRepo().DeleteByServerID(id); err != nil { + return err + } + + return app.Orm.Where("id = ?", id).Delete(&biz.DatabaseServer{}).Error +} + +func (r databaseServerRepo) Sync(id uint) error { + server, err := r.Get(id) + if err != nil { + return err + } + + users := make([]*biz.DatabaseUser, 0) + if err = app.Orm.Where("server_id = ?", id).Find(&users).Error; err != nil { + return err + } + + switch server.Type { + case biz.DatabaseTypeMysql: + mysql, err := db.NewMySQL(server.Username, server.Password, fmt.Sprintf("%s:%d", server.Host, server.Port)) + if err != nil { + return err + } + allUsers, err := mysql.Users() + if err != nil { + return err + } + for user := range slices.Values(allUsers) { + if !slices.ContainsFunc(users, func(a *biz.DatabaseUser) bool { + return a.Username == user.User && a.Host == user.Host + }) { + newUser := &biz.DatabaseUser{ + ServerID: id, + Username: user.User, + Host: user.Host, + Remark: fmt.Sprintf("sync from server %s", server.Name), + } + app.Orm.Create(newUser) + } + } + case biz.DatabaseTypePostgresql: + postgres, err := db.NewPostgres(server.Username, server.Password, server.Host, server.Port) + if err != nil { + return err + } + allUsers, err := postgres.Users() + if err != nil { + return err + } + for user := range slices.Values(allUsers) { + if !slices.ContainsFunc(users, func(a *biz.DatabaseUser) bool { + return a.Username == user.Role + }) { + newUser := &biz.DatabaseUser{ + ServerID: id, + Username: user.Role, + Remark: fmt.Sprintf("sync from server %s", server.Name), + } + app.Orm.Create(newUser) + } + } + } + + return nil +} + +// checkServer 检查服务器连接 +func (r databaseServerRepo) checkServer(server *biz.DatabaseServer) bool { + switch server.Type { + case biz.DatabaseTypeMysql: + _, err := db.NewMySQL(server.Username, server.Password, fmt.Sprintf("%s:%d", server.Host, server.Port)) + if err == nil { + server.Status = biz.DatabaseServerStatusValid + return true + } + case biz.DatabaseTypePostgresql: + _, err := db.NewPostgres(server.Username, server.Password, server.Host, server.Port) + if err == nil { + server.Status = biz.DatabaseServerStatusValid + return true + } + case biz.DatabaseTypeRedis: + _, err := db.NewRedis(server.Username, server.Password, fmt.Sprintf("%s:%d", server.Host, server.Port)) + if err == nil { + server.Status = biz.DatabaseServerStatusValid + return true + } + } + + server.Status = biz.DatabaseServerStatusInvalid + return false } diff --git a/internal/data/database_user.go b/internal/data/database_user.go new file mode 100644 index 00000000..824fc574 --- /dev/null +++ b/internal/data/database_user.go @@ -0,0 +1,213 @@ +package data + +import ( + "fmt" + "slices" + + "github.com/samber/do/v2" + + "github.com/TheTNB/panel/internal/app" + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/http/request" + "github.com/TheTNB/panel/pkg/db" +) + +type databaseUserRepo struct{} + +func NewDatabaseUserRepo() biz.DatabaseUserRepo { + return do.MustInvoke[biz.DatabaseUserRepo](injector) +} + +func (r databaseUserRepo) Count() (int64, error) { + var count int64 + if err := app.Orm.Model(&biz.DatabaseUser{}).Count(&count).Error; err != nil { + return 0, err + } + + return count, nil +} + +func (r databaseUserRepo) List(page, limit uint) ([]*biz.DatabaseUser, int64, error) { + var user []*biz.DatabaseUser + var total int64 + err := app.Orm.Model(&biz.DatabaseUser{}).Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&user).Error + + for u := range slices.Values(user) { + r.fillUser(u) + } + + return user, total, err +} + +func (r databaseUserRepo) Get(id uint) (*biz.DatabaseUser, error) { + user := new(biz.DatabaseUser) + if err := app.Orm.Where("id = ?", id).First(user).Error; err != nil { + return nil, err + } + + r.fillUser(user) + + return user, nil +} + +func (r databaseUserRepo) Create(req *request.DatabaseUserCreate) error { + server, err := NewDatabaseServerRepo().Get(req.ServerID) + if err != nil { + return err + } + + switch server.Type { + case biz.DatabaseTypeMysql: + mysql, err := db.NewMySQL(server.Username, server.Password, fmt.Sprintf("%s:%d", server.Host, server.Port)) + if err != nil { + return err + } + if err = mysql.UserCreate(req.Username, req.Password); err != nil { + return err + } + for name := range slices.Values(req.Privileges) { + if err = mysql.PrivilegesGrant(req.Username, name); err != nil { + return err + } + } + case biz.DatabaseTypePostgresql: + postgres, err := db.NewPostgres(server.Username, server.Password, server.Host, server.Port) + if err != nil { + return err + } + if err = postgres.UserCreate(req.Username, req.Password); err != nil { + return err + } + for name := range slices.Values(req.Privileges) { + if err = postgres.PrivilegesGrant(req.Username, name); err != nil { + return err + } + } + } + + user := &biz.DatabaseUser{ + ServerID: req.ServerID, + Username: req.Username, + Password: req.Password, + Remark: req.Remark, + } + + return app.Orm.Create(user).Error +} + +func (r databaseUserRepo) Update(req *request.DatabaseUserUpdate) error { + user, err := r.Get(req.ID) + if err != nil { + return err + } + + server, err := NewDatabaseServerRepo().Get(user.ServerID) + if err != nil { + return err + } + + switch server.Type { + case biz.DatabaseTypeMysql: + mysql, err := db.NewMySQL(server.Username, server.Password, fmt.Sprintf("%s:%d", server.Host, server.Port)) + if err != nil { + return err + } + if req.Password != "" { + if err = mysql.UserPassword(user.Username, req.Password); err != nil { + return err + } + } + for name := range slices.Values(req.Privileges) { + if err = mysql.PrivilegesGrant(user.Username, name); err != nil { + return err + } + } + case biz.DatabaseTypePostgresql: + postgres, err := db.NewPostgres(server.Username, server.Password, server.Host, server.Port) + if err != nil { + return err + } + if req.Password != "" { + if err = postgres.UserPassword(user.Username, req.Password); err != nil { + return err + } + } + for name := range slices.Values(req.Privileges) { + if err = postgres.PrivilegesGrant(user.Username, name); err != nil { + return err + } + } + } + + user.Password = req.Password + user.Remark = req.Remark + + return app.Orm.Save(user).Error +} + +func (r databaseUserRepo) Delete(id uint) error { + user, err := r.Get(id) + if err != nil { + return err + } + + server, err := NewDatabaseServerRepo().Get(user.ServerID) + if err != nil { + return err + } + + switch server.Type { + case biz.DatabaseTypeMysql: + mysql, err := db.NewMySQL(server.Username, server.Password, fmt.Sprintf("%s:%d", server.Host, server.Port)) + if err != nil { + return err + } + _ = mysql.UserDrop(user.Username) + case biz.DatabaseTypePostgresql: + postgres, err := db.NewPostgres(server.Username, server.Password, server.Host, server.Port) + if err != nil { + return err + } + _ = postgres.DatabaseDrop(user.Username) + } + + return app.Orm.Where("id = ?", id).Delete(&biz.DatabaseUser{}).Error +} + +func (r databaseUserRepo) DeleteByServerID(serverID uint) error { + return app.Orm.Where("server_id = ?", serverID).Delete(&biz.DatabaseUser{}).Error +} + +func (r databaseUserRepo) fillUser(user *biz.DatabaseUser) { + server, err := NewDatabaseServerRepo().Get(user.ServerID) + if err == nil { + switch server.Type { + case biz.DatabaseTypeMysql: + mysql, err := db.NewMySQL(server.Username, server.Password, fmt.Sprintf("%s:%d", server.Host, server.Port)) + if err == nil { + privileges, _ := mysql.UserPrivileges(user.Username, user.Host) + user.Privileges = privileges + } + if _, err := db.NewMySQL(user.Username, user.Password, fmt.Sprintf("%s:%d", server.Host, server.Port)); err == nil { + user.Status = biz.DatabaseUserStatusValid + } else { + user.Status = biz.DatabaseUserStatusInvalid + } + case biz.DatabaseTypePostgresql: + postgres, err := db.NewPostgres(server.Username, server.Password, server.Host, server.Port) + if err == nil { + privileges, _ := postgres.UserPrivileges(user.Username) + user.Privileges = privileges + } + if _, err := db.NewPostgres(user.Username, user.Password, server.Host, server.Port); err == nil { + user.Status = biz.DatabaseUserStatusValid + } else { + user.Status = biz.DatabaseUserStatusInvalid + } + } + } + // 初始化,防止 nil + if user.Privileges == nil { + user.Privileges = make(map[string][]string) + } +} diff --git a/internal/data/init.go b/internal/data/init.go index 40f0e468..6fcdb6db 100644 --- a/internal/data/init.go +++ b/internal/data/init.go @@ -66,7 +66,7 @@ func init() { return &databaseServerRepo{}, nil }) do.Provide(injector, func(i do.Injector) (biz.DatabaseUserRepo, error) { - return nil, nil // TODO + return &databaseUserRepo{}, nil }) do.Provide(injector, func(i do.Injector) (biz.DatabaseRepo, error) { return &databaseRepo{}, nil diff --git a/internal/http/request/database_server.go b/internal/http/request/database_server.go index 7087f45b..6b35401f 100644 --- a/internal/http/request/database_server.go +++ b/internal/http/request/database_server.go @@ -13,7 +13,6 @@ type DatabaseServerCreate struct { type DatabaseServerUpdate struct { ID uint `form:"id" json:"id" validate:"required,exists=database_servers id"` Name string `form:"name" json:"name" validate:"required,not_exists=database_servers name"` - Type string `form:"type" json:"type" validate:"required,oneof=mysql postgresql redis"` Host string `form:"host" json:"host" validate:"required"` Port uint `form:"port" json:"port" validate:"required,number,gte=1,lte=65535"` Username string `form:"username" json:"username"` diff --git a/internal/http/request/database_user.go b/internal/http/request/database_user.go index 57a1a435..753aba68 100644 --- a/internal/http/request/database_user.go +++ b/internal/http/request/database_user.go @@ -9,8 +9,7 @@ type DatabaseUserCreate struct { } type DatabaseUserUpdate struct { - ID string `form:"id" json:"id" validate:"required,exists=database_users id"` - Username string `form:"username" json:"username"` + ID uint `form:"id" json:"id" validate:"required,exists=database_users id"` Password string `form:"password" json:"password"` Privileges []string `form:"privileges" json:"privileges"` Remark string `form:"remark" json:"remark"` diff --git a/internal/route/http.go b/internal/route/http.go index 4e0ff84c..41ad209c 100644 --- a/internal/route/http.go +++ b/internal/route/http.go @@ -70,11 +70,20 @@ func Http(r chi.Router) { }) r.Route("/databaseServer", func(r chi.Router) { - database := service.NewDatabaseServerService() - r.Get("/", database.List) - r.Post("/", database.Create) - r.Put("/{id}", database.Update) - r.Delete("/{id}", database.Delete) + server := service.NewDatabaseServerService() + r.Get("/", server.List) + r.Post("/", server.Create) + r.Put("/{id}", server.Update) + r.Delete("/{id}", server.Delete) + r.Delete("/{id}/sync", server.Sync) + }) + + r.Route("/databaseUser", func(r chi.Router) { + user := service.NewDatabaseUserService() + r.Get("/", user.List) + r.Post("/", user.Create) + r.Put("/{id}", user.Update) + r.Delete("/{id}", user.Delete) }) r.Route("/backup", func(r chi.Router) { diff --git a/internal/service/database.go b/internal/service/database.go index a59b1b5a..d12abfc2 100644 --- a/internal/service/database.go +++ b/internal/service/database.go @@ -27,7 +27,7 @@ func (s *Database) List(w http.ResponseWriter, r *http.Request) { return } - certs, total, err := s.databaseRepo.List(req.Page, req.Limit) + databases, total, err := s.databaseRepo.List(req.Page, req.Limit) if err != nil { Error(w, http.StatusInternalServerError, "%v", err) return @@ -35,7 +35,7 @@ func (s *Database) List(w http.ResponseWriter, r *http.Request) { Success(w, chix.M{ "total": total, - "items": certs, + "items": databases, }) } diff --git a/internal/service/database_server.go b/internal/service/database_server.go index e2bbe519..a6931aaa 100644 --- a/internal/service/database_server.go +++ b/internal/service/database_server.go @@ -27,7 +27,7 @@ func (s *DatabaseServer) List(w http.ResponseWriter, r *http.Request) { return } - certs, total, err := s.databaseServerRepo.List(req.Page, req.Limit) + servers, total, err := s.databaseServerRepo.List(req.Page, req.Limit) if err != nil { Error(w, http.StatusInternalServerError, "%v", err) return @@ -35,7 +35,7 @@ func (s *DatabaseServer) List(w http.ResponseWriter, r *http.Request) { Success(w, chix.M{ "total": total, - "items": certs, + "items": servers, }) } @@ -83,3 +83,18 @@ func (s *DatabaseServer) Delete(w http.ResponseWriter, r *http.Request) { Success(w, nil) } + +func (s *DatabaseServer) Sync(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, "%v", err) + return + } + + if err = s.databaseServerRepo.Sync(req.ID); err != nil { + Error(w, http.StatusInternalServerError, "%v", err) + return + } + + Success(w, nil) +} diff --git a/internal/service/database_user.go b/internal/service/database_user.go new file mode 100644 index 00000000..2200b641 --- /dev/null +++ b/internal/service/database_user.go @@ -0,0 +1,85 @@ +package service + +import ( + "net/http" + + "github.com/go-rat/chix" + + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/data" + "github.com/TheTNB/panel/internal/http/request" +) + +type DatabaseUser struct { + databaseUserRepo biz.DatabaseUserRepo +} + +func NewDatabaseUserService() *DatabaseUser { + return &DatabaseUser{ + databaseUserRepo: data.NewDatabaseUserRepo(), + } +} + +func (s *DatabaseUser) List(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.Paginate](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, "%v", err) + return + } + + users, total, err := s.databaseUserRepo.List(req.Page, req.Limit) + if err != nil { + Error(w, http.StatusInternalServerError, "%v", err) + return + } + + Success(w, chix.M{ + "total": total, + "items": users, + }) +} + +func (s *DatabaseUser) Create(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.DatabaseUserCreate](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, "%v", err) + return + } + + if err = s.databaseUserRepo.Create(req); err != nil { + Error(w, http.StatusInternalServerError, "%v", err) + return + } + + Success(w, nil) +} + +func (s *DatabaseUser) Update(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.DatabaseUserUpdate](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, "%v", err) + return + } + + if err = s.databaseUserRepo.Update(req); err != nil { + Error(w, http.StatusInternalServerError, "%v", err) + return + } + + Success(w, nil) +} + +func (s *DatabaseUser) Delete(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, "%v", err) + return + } + + if err = s.databaseUserRepo.Delete(req.ID); err != nil { + Error(w, http.StatusInternalServerError, "%v", err) + return + } + + Success(w, nil) +} diff --git a/pkg/db/mysql.go b/pkg/db/mysql.go index 722bfee1..d9d9f62e 100644 --- a/pkg/db/mysql.go +++ b/pkg/db/mysql.go @@ -4,6 +4,7 @@ import ( "database/sql" "fmt" "slices" + "strings" _ "github.com/go-sql-driver/mysql" @@ -132,15 +133,33 @@ func (m *MySQL) UserPrivileges(user, host string) (map[string][]string, error) { privileges := make(map[string][]string) for rows.Next() { var grant string - if err := rows.Scan(&grant); err != nil { + if err = rows.Scan(&grant); err != nil { + return nil, fmt.Errorf("failed to scan grant: %w", err) + } + if !strings.HasPrefix(grant, "GRANT ") { continue } - var db string - var privs []string - if _, err := fmt.Sscanf(grant, "GRANT %s ON %s TO", &privs, &db); err == nil { - privileges[db] = append(privileges[db], privs...) + parts := strings.Split(grant, " ON ") + if len(parts) < 2 { + continue } + + privList := strings.TrimPrefix(parts[0], "GRANT ") + privs := strings.Split(privList, ", ") + + dbPart := strings.Split(parts[1], " TO")[0] + // *.* 表示全局权限 + if dbPart == "*.*" { + dbPart = "*" + } + + dbPart = strings.Trim(dbPart, "`") + privileges[dbPart] = append(privileges[dbPart], privs...) + } + + if err = rows.Err(); err != nil { + return nil, err } return privileges, nil diff --git a/pkg/db/postgres.go b/pkg/db/postgres.go index a2e10a7d..a2314616 100644 --- a/pkg/db/postgres.go +++ b/pkg/db/postgres.go @@ -43,59 +43,59 @@ func NewPostgres(username, password, address string, port uint) (*Postgres, erro }, nil } -func (m *Postgres) Close() error { - return m.db.Close() +func (r *Postgres) Close() error { + return r.db.Close() } -func (m *Postgres) Ping() error { - return m.db.Ping() +func (r *Postgres) Ping() error { + return r.db.Ping() } -func (m *Postgres) Query(query string, args ...any) (*sql.Rows, error) { - return m.db.Query(query, args...) +func (r *Postgres) Query(query string, args ...any) (*sql.Rows, error) { + return r.db.Query(query, args...) } -func (m *Postgres) QueryRow(query string, args ...any) *sql.Row { - return m.db.QueryRow(query, args...) +func (r *Postgres) QueryRow(query string, args ...any) *sql.Row { + return r.db.QueryRow(query, args...) } -func (m *Postgres) Exec(query string, args ...any) (sql.Result, error) { - return m.db.Exec(query, args...) +func (r *Postgres) Exec(query string, args ...any) (sql.Result, error) { + return r.db.Exec(query, args...) } -func (m *Postgres) Prepare(query string) (*sql.Stmt, error) { - return m.db.Prepare(query) +func (r *Postgres) Prepare(query string) (*sql.Stmt, error) { + return r.db.Prepare(query) } -func (m *Postgres) DatabaseCreate(name string) error { - _, err := m.Exec(fmt.Sprintf("CREATE DATABASE %s", name)) +func (r *Postgres) DatabaseCreate(name string) error { + _, err := r.Exec(fmt.Sprintf("CREATE DATABASE %s", name)) return err } -func (m *Postgres) DatabaseDrop(name string) error { - _, err := m.Exec(fmt.Sprintf("DROP DATABASE IF EXISTS %s", name)) +func (r *Postgres) DatabaseDrop(name string) error { + _, err := r.Exec(fmt.Sprintf("DROP DATABASE IF EXISTS %s", name)) return err } -func (m *Postgres) DatabaseExist(name string) (bool, error) { +func (r *Postgres) DatabaseExist(name string) (bool, error) { var count int - if err := m.QueryRow("SELECT COUNT(*) FROM pg_database WHERE datname = $1", name).Scan(&count); err != nil { + if err := r.QueryRow("SELECT COUNT(*) FROM pg_database WHERE datname = $1", name).Scan(&count); err != nil { return false, err } return count > 0, nil } -func (m *Postgres) DatabaseSize(name string) (int64, error) { +func (r *Postgres) DatabaseSize(name string) (int64, error) { query := fmt.Sprintf("SELECT pg_database_size('%s')", name) var size int64 - if err := m.QueryRow(query).Scan(&size); err != nil { + if err := r.QueryRow(query).Scan(&size); err != nil { return 0, err } return size, nil } -func (m *Postgres) UserCreate(user, password string) error { - _, err := m.Exec(fmt.Sprintf("CREATE USER %s WITH PASSWORD '%s'", user, password)) +func (r *Postgres) UserCreate(user, password string) error { + _, err := r.Exec(fmt.Sprintf("CREATE USER %s WITH PASSWORD '%s'", user, password)) if err != nil { return err } @@ -103,8 +103,8 @@ func (m *Postgres) UserCreate(user, password string) error { return nil } -func (m *Postgres) UserDrop(user string) error { - _, err := m.Exec(fmt.Sprintf("DROP USER IF EXISTS %s", user)) +func (r *Postgres) UserDrop(user string) error { + _, err := r.Exec(fmt.Sprintf("DROP USER IF EXISTS %s", user)) if err != nil { return err } @@ -112,12 +112,12 @@ func (m *Postgres) UserDrop(user string) error { return systemctl.Reload("postgresql") } -func (m *Postgres) UserPassword(user, password string) error { - _, err := m.Exec(fmt.Sprintf("ALTER USER %s WITH PASSWORD '%s'", user, password)) +func (r *Postgres) UserPassword(user, password string) error { + _, err := r.Exec(fmt.Sprintf("ALTER USER %s WITH PASSWORD '%s'", user, password)) return err } -func (p *Postgres) UserPrivileges(user string) (map[string][]string, error) { +func (r *Postgres) UserPrivileges(user string) (map[string][]string, error) { query := ` SELECT table_catalog as database_name, @@ -126,7 +126,7 @@ func (p *Postgres) UserPrivileges(user string) (map[string][]string, error) { WHERE grantee = $1 GROUP BY table_catalog` - rows, err := p.Query(query, user) + rows, err := r.Query(query, user) if err != nil { return nil, fmt.Errorf("failed to query database privileges: %w", err) } @@ -135,39 +135,38 @@ func (p *Postgres) UserPrivileges(user string) (map[string][]string, error) { privileges := make(map[string][]string) for rows.Next() { - var dbName, privilegeStr string - if err := rows.Scan(&dbName, &privilegeStr); err != nil { + var db, privilegeStr string + if err := rows.Scan(&db, &privilegeStr); err != nil { return nil, fmt.Errorf("failed to scan row: %w", err) } - key := fmt.Sprintf("%s.*", dbName) - privileges[key] = strings.Split(privilegeStr, ",") + privileges[db] = strings.Split(privilegeStr, ",") } if err = rows.Err(); err != nil { - return nil, fmt.Errorf("error iterating rows: %w", err) + return nil, err } return privileges, nil } -func (m *Postgres) PrivilegesGrant(user, database string) error { - if _, err := m.Exec(fmt.Sprintf("ALTER DATABASE %s OWNER TO %s", database, user)); err != nil { +func (r *Postgres) PrivilegesGrant(user, database string) error { + if _, err := r.Exec(fmt.Sprintf("ALTER DATABASE %s OWNER TO %s", database, user)); err != nil { return err } - if _, err := m.Exec(fmt.Sprintf("GRANT ALL PRIVILEGES ON DATABASE %s TO %s", database, user)); err != nil { + if _, err := r.Exec(fmt.Sprintf("GRANT ALL PRIVILEGES ON DATABASE %s TO %s", database, user)); err != nil { return err } return nil } -func (m *Postgres) PrivilegesRevoke(user, database string) error { - _, err := m.Exec(fmt.Sprintf("REVOKE ALL PRIVILEGES ON DATABASE %s FROM %s", database, user)) +func (r *Postgres) PrivilegesRevoke(user, database string) error { + _, err := r.Exec(fmt.Sprintf("REVOKE ALL PRIVILEGES ON DATABASE %s FROM %s", database, user)) return err } -func (m *Postgres) Users() ([]types.PostgresUser, error) { +func (r *Postgres) Users() ([]types.PostgresUser, error) { query := ` SELECT rolname, rolsuper, @@ -178,7 +177,7 @@ func (m *Postgres) Users() ([]types.PostgresUser, error) { FROM pg_roles WHERE rolcanlogin = true; ` - rows, err := m.Query(query) + rows, err := r.Query(query) if err != nil { return nil, err } @@ -212,16 +211,20 @@ func (m *Postgres) Users() ([]types.PostgresUser, error) { users = append(users, user) } + if err = rows.Err(); err != nil { + return nil, err + } + return users, nil } -func (m *Postgres) Databases() ([]types.PostgresDatabase, error) { +func (r *Postgres) Databases() ([]types.PostgresDatabase, error) { query := ` SELECT d.datname, pg_catalog.pg_get_userbyid(d.datdba), pg_catalog.pg_encoding_to_char(d.encoding) FROM pg_catalog.pg_database d WHERE datistemplate = false; ` - rows, err := m.Query(query) + rows, err := r.Query(query) if err != nil { return nil, err } diff --git a/pkg/types/database.go b/pkg/types/database.go deleted file mode 100644 index 18523bac..00000000 --- a/pkg/types/database.go +++ /dev/null @@ -1,15 +0,0 @@ -package types - -type DatabaseStatus string - -const ( - DatabaseStatusValid DatabaseStatus = "valid" - DatabaseStatusInvalid DatabaseStatus = "invalid" -) - -type Database struct { - Name string `json:"name"` - ServerID uint `json:"server_id"` - Status DatabaseStatus `json:"status"` - Remark string `json:"remark"` -} diff --git a/web/src/api/panel/database/index.ts b/web/src/api/panel/database/index.ts index 635b0b03..6b5bf83a 100644 --- a/web/src/api/panel/database/index.ts +++ b/web/src/api/panel/database/index.ts @@ -19,5 +19,13 @@ export default { // 删除数据库服务器 serverDelete: (id: number) => http.Delete(`/databaseServer/${id}`), // 同步数据库 - serverSync: (id: number) => http.Post(`/databaseServer/${id}/sync`) + serverSync: (id: number) => http.Post(`/databaseServer/${id}/sync`), + // 获取数据库用户列表 + userList: (page: number, limit: number) => http.Get('/databaseUser', { params: { page, limit } }), + // 创建数据库用户 + userCreate: (data: any) => http.Post('/databaseUser', data), + // 更新数据库用户 + userUpdate: (id: number, data: any) => http.Put(`/databaseUser/${id}`, data), + // 删除数据库用户 + userDelete: (id: number) => http.Delete(`/databaseUser/${id}`) }