diff --git a/cmd/dashboard/controller/controller.go b/cmd/dashboard/controller/controller.go index 9f0f4af..333b632 100644 --- a/cmd/dashboard/controller/controller.go +++ b/cmd/dashboard/controller/controller.go @@ -9,6 +9,7 @@ import ( "net/http" "os" "path" + "slices" "strings" jwt "github.com/appleboy/gin-jwt/v2" @@ -82,7 +83,7 @@ func routers(r *gin.Engine, frontendDist fs.FS) { auth.POST("/user", commonHandler(createUser)) auth.POST("/batch-delete/user", commonHandler(batchDeleteUser)) - auth.GET("/service/list", commonHandler(listService)) + auth.GET("/service/list", listHandler(listService)) auth.POST("/service", commonHandler(createService)) auth.PATCH("/service/:id", commonHandler(updateService)) auth.POST("/batch-delete/service", commonHandler(batchDeleteService)) @@ -96,34 +97,34 @@ func routers(r *gin.Engine, frontendDist fs.FS) { auth.PATCH("/notification-group/:id", commonHandler(updateNotificationGroup)) auth.POST("/batch-delete/notification-group", commonHandler(batchDeleteNotificationGroup)) - auth.GET("/server", commonHandler(listServer)) + auth.GET("/server", listHandler(listServer)) auth.PATCH("/server/:id", commonHandler(updateServer)) auth.POST("/batch-delete/server", commonHandler(batchDeleteServer)) auth.POST("/force-update/server", commonHandler(forceUpdateServer)) - auth.GET("/notification", commonHandler(listNotification)) + auth.GET("/notification", listHandler(listNotification)) auth.POST("/notification", commonHandler(createNotification)) auth.PATCH("/notification/:id", commonHandler(updateNotification)) auth.POST("/batch-delete/notification", commonHandler(batchDeleteNotification)) - auth.GET("/alert-rule", commonHandler(listAlertRule)) + auth.GET("/alert-rule", listHandler(listAlertRule)) auth.POST("/alert-rule", commonHandler(createAlertRule)) auth.PATCH("/alert-rule/:id", commonHandler(updateAlertRule)) auth.POST("/batch-delete/alert-rule", commonHandler(batchDeleteAlertRule)) - auth.GET("/cron", commonHandler(listCron)) + auth.GET("/cron", listHandler(listCron)) auth.POST("/cron", commonHandler(createCron)) auth.PATCH("/cron/:id", commonHandler(updateCron)) auth.GET("/cron/:id/manual", commonHandler(manualTriggerCron)) auth.POST("/batch-delete/cron", commonHandler(batchDeleteCron)) - auth.GET("/ddns", commonHandler(listDDNS)) + auth.GET("/ddns", listHandler(listDDNS)) auth.GET("/ddns/providers", commonHandler(listProviders)) auth.POST("/ddns", commonHandler(createDDNS)) auth.PATCH("/ddns/:id", commonHandler(updateDDNS)) auth.POST("/batch-delete/ddns", commonHandler(batchDeleteDDNS)) - auth.GET("/nat", commonHandler(listNAT)) + auth.GET("/nat", listHandler(listNAT)) auth.POST("/nat", commonHandler(createNAT)) auth.PATCH("/nat/:id", commonHandler(updateNAT)) auth.POST("/batch-delete/nat", commonHandler(batchDeleteNAT)) @@ -212,6 +213,29 @@ func commonHandler[T any](handler handlerFunc[T]) func(*gin.Context) { } } +func listHandler[S ~[]E, E model.CommonInterface](handler handlerFunc[S]) func(*gin.Context) { + return func(c *gin.Context) { + data, err := handler(c) + if err != nil { + c.JSON(http.StatusOK, newErrorResponse(err)) + return + } + + c.JSON(http.StatusOK, filter(c, data)) + } +} + +func filter[S ~[]E, E model.CommonInterface](ctx *gin.Context, s S) S { + return slices.DeleteFunc(s, func(e E) bool { + return e.HasPermission(ctx) + }) +} + +func getUid(c *gin.Context) uint64 { + user, _ := c.MustGet(model.CtxKeyAuthorizedUser).(*model.User) + return user.ID +} + func fallbackToFrontend(frontendDist fs.FS) func(*gin.Context) { checkLocalFileOrFs := func(c *gin.Context, fs fs.FS, path string) bool { if _, err := os.Stat(path); err == nil { diff --git a/cmd/dashboard/controller/notification_group.go b/cmd/dashboard/controller/notification_group.go index 2e74dba..310e6bf 100644 --- a/cmd/dashboard/controller/notification_group.go +++ b/cmd/dashboard/controller/notification_group.go @@ -20,7 +20,7 @@ import ( // @Produce json // @Success 200 {object} model.CommonResponse[[]model.NotificationGroupResponseItem] // @Router /notification-group [get] -func listNotificationGroup(c *gin.Context) ([]model.NotificationGroupResponseItem, error) { +func listNotificationGroup(c *gin.Context) ([]*model.NotificationGroupResponseItem, error) { var ng []model.NotificationGroup if err := singleton.DB.Find(&ng).Error; err != nil { return nil, err @@ -39,9 +39,9 @@ func listNotificationGroup(c *gin.Context) ([]model.NotificationGroupResponseIte groupNotifications[n.NotificationGroupID] = append(groupNotifications[n.NotificationGroupID], n.NotificationID) } - ngRes := make([]model.NotificationGroupResponseItem, 0, len(ng)) + ngRes := make([]*model.NotificationGroupResponseItem, 0, len(ng)) for _, n := range ng { - ngRes = append(ngRes, model.NotificationGroupResponseItem{ + ngRes = append(ngRes, &model.NotificationGroupResponseItem{ Group: n, Notifications: groupNotifications[n.ID], }) diff --git a/cmd/dashboard/controller/server.go b/cmd/dashboard/controller/server.go index 48bd56e..604b0bb 100644 --- a/cmd/dashboard/controller/server.go +++ b/cmd/dashboard/controller/server.go @@ -61,6 +61,10 @@ func updateServer(c *gin.Context) (any, error) { return nil, singleton.Localizer.ErrorT("server id %d does not exist", id) } + if !s.HasPermission(c) { + return nil, singleton.Localizer.ErrorT("unauthorized") + } + s.Name = sf.Name s.DisplayIndex = sf.DisplayIndex s.Note = sf.Note @@ -99,11 +103,23 @@ func updateServer(c *gin.Context) (any, error) { // @Success 200 {object} model.CommonResponse[any] // @Router /batch-delete/server [post] func batchDeleteServer(c *gin.Context) (any, error) { - var servers []uint64 - if err := c.ShouldBindJSON(&servers); err != nil { + var serversRaw []uint64 + if err := c.ShouldBindJSON(&serversRaw); err != nil { return nil, err } + var servers []uint64 + singleton.ServerLock.RLock() + for _, sid := range serversRaw { + if s, ok := singleton.ServerList[sid]; ok { + if !s.HasPermission(c) { + return nil, singleton.Localizer.ErrorT("permission denied") + } + servers = append(servers, s.ID) + } + } + singleton.ServerLock.RUnlock() + err := singleton.DB.Transaction(func(tx *gorm.DB) error { if err := tx.Unscoped().Delete(&model.Server{}, "id in (?)", servers).Error; err != nil { return err diff --git a/cmd/dashboard/controller/server_group.go b/cmd/dashboard/controller/server_group.go index 98b5c6f..c66bb39 100644 --- a/cmd/dashboard/controller/server_group.go +++ b/cmd/dashboard/controller/server_group.go @@ -20,7 +20,7 @@ import ( // @Produce json // @Success 200 {object} model.CommonResponse[[]model.ServerGroupResponseItem] // @Router /server-group [get] -func listServerGroup(c *gin.Context) ([]model.ServerGroupResponseItem, error) { +func listServerGroup(c *gin.Context) ([]*model.ServerGroupResponseItem, error) { var sg []model.ServerGroup if err := singleton.DB.Find(&sg).Error; err != nil { return nil, err @@ -38,9 +38,9 @@ func listServerGroup(c *gin.Context) ([]model.ServerGroupResponseItem, error) { groupServers[s.ServerGroupId] = append(groupServers[s.ServerGroupId], s.ServerId) } - var sgRes []model.ServerGroupResponseItem + var sgRes []*model.ServerGroupResponseItem for _, s := range sg { - sgRes = append(sgRes, model.ServerGroupResponseItem{ + sgRes = append(sgRes, &model.ServerGroupResponseItem{ Group: s, Servers: groupServers[s.ID], }) diff --git a/cmd/dashboard/controller/service.go b/cmd/dashboard/controller/service.go index 2956681..ed9e859 100644 --- a/cmd/dashboard/controller/service.go +++ b/cmd/dashboard/controller/service.go @@ -190,7 +190,10 @@ func createService(c *gin.Context) (uint64, error) { return 0, err } + uid := getUid(c) + var m model.Service + m.UserID = uid m.Name = mf.Name m.Target = strings.TrimSpace(mf.Target) m.Type = mf.Type diff --git a/model/common.go b/model/common.go index 10394e9..6b83911 100644 --- a/model/common.go +++ b/model/common.go @@ -2,6 +2,8 @@ package model import ( "time" + + "github.com/gin-gonic/gin" ) const ( @@ -17,6 +19,31 @@ type Common struct { UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at,omitempty"` // Do not use soft deletion // DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at,omitempty"` + + UserID uint64 `json:"user_id,omitempty"` +} + +func (c *Common) GetID() uint64 { + return c.ID +} + +func (c *Common) HasPermission(ctx *gin.Context) bool { + auth, ok := ctx.Get(CtxKeyAuthorizedUser) + if !ok { + return false + } + + user := *auth.(*User) + if user.Role == RoleAdmin { + return true + } + + return user.ID == c.UserID +} + +type CommonInterface interface { + GetID() uint64 + HasPermission(*gin.Context) bool } type Response struct { diff --git a/model/user.go b/model/user.go index e1f297b..fe8a1ed 100644 --- a/model/user.go +++ b/model/user.go @@ -1,9 +1,15 @@ package model +const ( + RoleAdmin uint8 = iota + RoleMember +) + type User struct { Common Username string `json:"username,omitempty" gorm:"uniqueIndex"` Password string `json:"password,omitempty" gorm:"type:char(72)"` + Role uint8 `json:"role,omitempty"` } type Profile struct {