diff --git a/cmd/server.go b/cmd/server.go index e3e4db9..66ed26a 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -29,6 +29,7 @@ var ServerCmd = &cobra.Command{ bootstrap.InitLog, bootstrap.InitGinMode, bootstrap.InitDatabase, + bootstrap.InitSetting, bootstrap.InitProvider, bootstrap.InitOp, bootstrap.InitRtmp, diff --git a/internal/bootstrap/setting.go b/internal/bootstrap/setting.go new file mode 100644 index 0000000..3968ae5 --- /dev/null +++ b/internal/bootstrap/setting.go @@ -0,0 +1,11 @@ +package bootstrap + +import ( + "context" + + "github.com/synctv-org/synctv/internal/setting" +) + +func InitSetting(ctx context.Context) error { + return setting.Init() +} diff --git a/internal/db/user.go b/internal/db/user.go index 075f5b5..fef9aba 100644 --- a/internal/db/user.go +++ b/internal/db/user.go @@ -245,3 +245,70 @@ func GetAdmins() []*model.User { db.Where("role >= ?", model.RoleAdmin).Find(&users) return users } + +func AddAdminByID(userID uint) error { + err := db.Model(&model.User{}).Where("id = ?", userID).Update("role", model.RoleAdmin).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("user not found") + } + return err +} + +func RemoveAdminByID(userID uint) error { + err := db.Model(&model.User{}).Where("id = ?", userID).Update("role", model.RoleUser).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("user not found") + } + return err +} + +func AddRoot(u *model.User) error { + if u.Role == model.RoleRoot { + return nil + } + u.Role = model.RoleRoot + return SaveUser(u) +} + +func RemoveRoot(u *model.User) error { + if u.Role != model.RoleRoot { + return nil + } + u.Role = model.RoleUser + return SaveUser(u) +} + +func AddRootByID(userID uint) error { + err := db.Model(&model.User{}).Where("id = ?", userID).Update("role", model.RoleRoot).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("user not found") + } + return err +} + +func RemoveRootByID(userID uint) error { + err := db.Model(&model.User{}).Where("id = ?", userID).Update("role", model.RoleUser).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("user not found") + } + return err +} + +func GetRoots() []*model.User { + var users []*model.User + db.Where("role = ?", model.RoleRoot).Find(&users) + return users +} + +func SetRole(u *model.User, role model.Role) error { + u.Role = role + return SaveUser(u) +} + +func SetRoleByID(userID uint, role model.Role) error { + err := db.Model(&model.User{}).Where("id = ?", userID).Update("role", role).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("user not found") + } + return err +} diff --git a/internal/model/user.go b/internal/model/user.go index 4d718f7..4e56904 100644 --- a/internal/model/user.go +++ b/internal/model/user.go @@ -14,6 +14,7 @@ const ( RoleBanned Role = iota RoleUser RoleAdmin + RoleRoot ) func (r Role) String() string { @@ -24,6 +25,8 @@ func (r Role) String() string { return "user" case RoleAdmin: return "admin" + case RoleRoot: + return "root" default: return "unknown" } @@ -50,6 +53,10 @@ func (u *User) BeforeCreate(tx *gorm.DB) error { return nil } +func (u *User) IsRoot() bool { + return u.Role == RoleRoot +} + func (u *User) IsAdmin() bool { return u.Role == RoleAdmin } diff --git a/internal/op/op.go b/internal/op/op.go index 64dabbf..8a8f93c 100644 --- a/internal/op/op.go +++ b/internal/op/op.go @@ -2,8 +2,6 @@ package op import ( "github.com/bluele/gcache" - "github.com/synctv-org/synctv/internal/db" - "github.com/synctv-org/synctv/internal/model" ) func Init(size int) error { @@ -11,27 +9,5 @@ func Init(size int) error { LRU(). Build() - err := initSettings(ToSettings(BoolSettings)...) - if err != nil { - return err - } - - return nil -} - -func initSettings(i ...Setting) error { - for _, b := range i { - s := &model.Setting{ - Name: b.Name(), - Value: b.Raw(), - Type: b.Type(), - Group: b.Group(), - } - err := db.FirstOrCreateSettingItemValue(s) - if err != nil { - return err - } - b.InitRaw(s.Value) - } return nil } diff --git a/internal/op/settings.go b/internal/op/settings.go deleted file mode 100644 index 7fa6b57..0000000 --- a/internal/op/settings.go +++ /dev/null @@ -1,150 +0,0 @@ -package op - -import ( - "github.com/synctv-org/synctv/internal/db" - "github.com/synctv-org/synctv/internal/model" -) - -var ( - BoolSettings map[string]BoolSetting -) - -type Setting interface { - Name() string - InitRaw(string) - Raw() string - Type() model.SettingType - Group() model.SettingGroup - Interface() (any, error) -} - -func GetSettingByGroup(group model.SettingGroup) []Setting { - return settingByGroup(group, ToSettings(BoolSettings)...) -} - -func settingByGroup(group model.SettingGroup, settings ...Setting) []Setting { - s := make([]Setting, 0, len(settings)) - for _, bs := range settings { - if bs.Group() == group { - s = append(s, bs) - } - } - return s -} - -func ToSettings[s Setting](settings ...map[string]s) []Setting { - l := 0 - for _, v := range settings { - l += len(v) - } - var ss []Setting = make([]Setting, 0, l) - for _, v := range settings { - for _, s := range v { - ss = append(ss, s) - } - } - return ss -} - -type BoolSetting interface { - Setting - Set(bool) error - Get() (bool, error) -} - -type Bool struct { - name string - value string -} - -func NewBool(name, value string) *Bool { - return &Bool{ - name: name, - value: value, - } -} - -func (b *Bool) Name() string { - return b.name -} - -func (b *Bool) InitRaw(s string) { - if b.value == s { - return - } - b.value = s -} - -func (b *Bool) Set(value bool) error { - if value { - if b.value == "1" { - return nil - } - b.value = "1" - } else { - if b.value == "0" { - return nil - } - b.value = "0" - } - return db.UpdateSettingItemValue(b.name, b.value) -} - -func (b *Bool) Get() (bool, error) { - return b.value == "1", nil -} - -func (b *Bool) Raw() string { - return b.value -} - -func (b *Bool) Type() model.SettingType { - return model.SettingTypeBool -} - -func (b *Bool) Group() model.SettingGroup { - return model.SettingGroupRoom -} - -func (b *Bool) Interface() (any, error) { - return b.Get() -} - -type Int64Setting interface { - Set(int64) error - Get() (int64, error) - Raw() string -} - -type Float64Setting interface { - Set(float64) error - Get() (float64, error) - Raw() string -} - -type StringSetting interface { - Set(string) error - Get() (string, error) - Raw() string -} - -func newRegBoolSetting(k, v string) BoolSetting { - b := NewBool(k, v) - if BoolSettings == nil { - BoolSettings = make(map[string]BoolSetting) - } - BoolSettings[k] = b - return b -} - -func GetSettingType(name string) (model.SettingType, bool) { - s, ok := BoolSettings[name] - if !ok { - return "", false - } - return s.Type(), true -} - -var ( - DisableCreateRoom = newRegBoolSetting("disable_create_room", "0") -) diff --git a/internal/op/user.go b/internal/op/user.go index 9207cf1..2f5ace4 100644 --- a/internal/op/user.go +++ b/internal/op/user.go @@ -22,8 +22,12 @@ func (u *User) NewMovie(movie model.MovieInfo) model.Movie { } } +func (u *User) IsRoot() bool { + return u.Role == model.RoleRoot +} + func (u *User) IsAdmin() bool { - return u.Role == model.RoleAdmin + return u.Role >= model.RoleAdmin } func (u *User) IsBanned() bool { @@ -31,7 +35,7 @@ func (u *User) IsBanned() bool { } func (u *User) HasPermission(roomID uint, permission model.Permission) bool { - if u.Role == model.RoleAdmin { + if u.Role >= model.RoleAdmin { return true } ur, err := db.GetRoomUserRelation(roomID, u.ID) @@ -54,3 +58,11 @@ func (u *User) SetRoomPassword(roomID uint, password string) error { } return SetRoomPassword(roomID, password) } + +func (u *User) SetRole(role model.Role) error { + if err := db.SetRoleByID(u.ID, role); err != nil { + return err + } + u.Role = role + return nil +} diff --git a/internal/setting/bool.go b/internal/setting/bool.go new file mode 100644 index 0000000..064519a --- /dev/null +++ b/internal/setting/bool.go @@ -0,0 +1,90 @@ +package setting + +import ( + log "github.com/sirupsen/logrus" + "github.com/synctv-org/synctv/internal/db" + "github.com/synctv-org/synctv/internal/model" +) + +type BoolSetting interface { + Setting + Set(bool) error + Get() (bool, error) +} + +type Bool struct { + name string + value string + group model.SettingGroup +} + +func NewBool(name, value string, group model.SettingGroup) *Bool { + return &Bool{ + name: name, + value: value, + group: group, + } +} + +func (b *Bool) Name() string { + return b.name +} + +func (b *Bool) InitRaw(s string) { + if b.value == s { + return + } + b.value = s +} + +func (b *Bool) Set(value bool) error { + if value { + if b.value == "1" { + return nil + } + b.value = "1" + } else { + if b.value == "0" { + return nil + } + b.value = "0" + } + return db.UpdateSettingItemValue(b.name, b.value) +} + +func (b *Bool) Get() (bool, error) { + return b.value == "1", nil +} + +func (b *Bool) Raw() string { + return b.value +} + +func (b *Bool) Type() model.SettingType { + return model.SettingTypeBool +} + +func (b *Bool) Group() model.SettingGroup { + return b.group +} + +func (b *Bool) Interface() (any, error) { + return b.Get() +} + +func newBoolSetting(k, v string, g model.SettingGroup) BoolSetting { + if Settings == nil { + Settings = make(map[string]Setting) + } + if GroupsSetting == nil { + GroupsSetting = make(map[model.SettingGroup][]Setting) + } + _, loaded := Settings[k] + if loaded { + log.Fatalf("setting %s already exists", k) + } + b := NewBool(k, v, g) + Settings[k] = b + GroupsSetting[g] = append(GroupsSetting[g], b) + return b +} diff --git a/internal/setting/setting.go b/internal/setting/setting.go new file mode 100644 index 0000000..ded6c11 --- /dev/null +++ b/internal/setting/setting.go @@ -0,0 +1,126 @@ +package setting + +import ( + "fmt" + + log "github.com/sirupsen/logrus" + "github.com/synctv-org/synctv/internal/db" + "github.com/synctv-org/synctv/internal/model" +) + +var ( + Settings map[string]Setting + GroupsSetting map[model.SettingGroup][]Setting +) + +type Setting interface { + Name() string + InitRaw(string) + Raw() string + Type() model.SettingType + Group() model.SettingGroup + Interface() (any, error) +} + +func SetValue(name string, value any) error { + s, ok := Settings[name] + if !ok { + return fmt.Errorf("setting %s not found", name) + } + return SetSettingValue(s, value) +} + +func SetSettingValue(s Setting, value any) error { + switch s.Type() { + case model.SettingTypeBool: + i, ok := s.(BoolSetting) + if !ok { + log.Fatalf("setting %s is not bool", s.Name()) + } + v, ok := value.(bool) + if !ok { + return fmt.Errorf("setting %s, value %v is not bool", s.Name(), value) + } + i.Set(v) + case model.SettingTypeInt64: + i, ok := s.(Int64Setting) + if !ok { + log.Fatalf("setting %s is not int64", s.Name()) + } + v, ok := value.(int64) + if !ok { + return fmt.Errorf("setting %s, value %v is not int64", s.Name(), value) + } + i.Set(v) + case model.SettingTypeFloat64: + i, ok := s.(Float64Setting) + if !ok { + log.Fatalf("setting %s is not float64", s.Name()) + } + v, ok := value.(float64) + if !ok { + return fmt.Errorf("setting %s, value %v is not float64", s.Name(), value) + } + i.Set(v) + case model.SettingTypeString: + i, ok := s.(StringSetting) + if !ok { + log.Fatalf("setting %s is not string", s.Name()) + } + v, ok := value.(string) + if !ok { + return fmt.Errorf("setting %s, value %v is not string", s.Name(), value) + } + i.Set(v) + default: + log.Fatalf("unknown setting type: %s", s.Type()) + } + return nil +} + +func ToSettings[s Setting](settings map[string]s) []Setting { + var ss []Setting = make([]Setting, 0, len(settings)) + for _, v := range settings { + ss = append(ss, v) + } + return ss +} + +type Int64Setting interface { + Set(int64) error + Get() (int64, error) + Raw() string +} + +type Float64Setting interface { + Set(float64) error + Get() (float64, error) + Raw() string +} + +type StringSetting interface { + Set(string) error + Get() (string, error) + Raw() string +} + +func Init() error { + return initSettings(ToSettings(Settings)...) +} + +func initSettings(i ...Setting) error { + for _, b := range i { + s := &model.Setting{ + Name: b.Name(), + Value: b.Raw(), + Type: b.Type(), + Group: b.Group(), + } + err := db.FirstOrCreateSettingItemValue(s) + if err != nil { + return err + } + b.InitRaw(s.Value) + } + return nil +} diff --git a/internal/setting/var.go b/internal/setting/var.go new file mode 100644 index 0000000..498d03c --- /dev/null +++ b/internal/setting/var.go @@ -0,0 +1,7 @@ +package setting + +import "github.com/synctv-org/synctv/internal/model" + +var ( + DisableCreateRoom = newBoolSetting("disable_create_room", "0", model.SettingGroupRoom) +) diff --git a/server/handlers/admin.go b/server/handlers/admin.go index 3bbcd96..ecd2a72 100644 --- a/server/handlers/admin.go +++ b/server/handlers/admin.go @@ -1,12 +1,12 @@ package handlers import ( - "fmt" "net/http" "github.com/gin-gonic/gin" dbModel "github.com/synctv-org/synctv/internal/model" "github.com/synctv-org/synctv/internal/op" + "github.com/synctv-org/synctv/internal/setting" "github.com/synctv-org/synctv/server/model" ) @@ -14,26 +14,17 @@ func EditAdminSettings(ctx *gin.Context) { // user := ctx.MustGet("user").(*op.User) req := model.AdminSettingsReq{} - if err := req.Decode(ctx); err != nil { - ctx.AbortWithError(http.StatusBadRequest, err) + if err := model.Decode(ctx, &req); err != nil { + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) return } for k, v := range req { - t, ok := op.GetSettingType(k) - if !ok { - ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp(fmt.Sprintf("setting %s not found", k))) + err := setting.SetValue(k, v) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) return } - switch t { - case dbModel.SettingTypeBool: - b, ok := v.(bool) - if !ok { - ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp(fmt.Sprintf("setting %s is not bool", k))) - return - } - op.BoolSettings[k].Set(b) - } } ctx.Status(http.StatusNoContent) @@ -41,13 +32,17 @@ func EditAdminSettings(ctx *gin.Context) { func AdminSettings(ctx *gin.Context) { // user := ctx.MustGet("user").(*op.User) - group := ctx.Query("group") + group := ctx.Param("group") if group == "" { ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("group is required")) return } - s := op.GetSettingByGroup(dbModel.SettingGroup(group)) + s, ok := setting.GroupsSetting[dbModel.SettingGroup(group)] + if !ok { + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("group not found")) + return + } resp := make(gin.H, len(s)) for _, v := range s { i, err := v.Interface() @@ -60,3 +55,47 @@ func AdminSettings(ctx *gin.Context) { ctx.JSON(http.StatusOK, model.NewApiDataResp(resp)) } + +func AddAdmin(ctx *gin.Context) { + user := ctx.MustGet("user").(*op.User) + + if !user.IsRoot() { + ctx.AbortWithStatusJSON(http.StatusForbidden, model.NewApiErrorStringResp("permission denied")) + return + } + + req := model.IdReq{} + if err := model.Decode(ctx, &req); err != nil { + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) + return + } + + if err := user.SetRole(dbModel.RoleAdmin); err != nil { + ctx.AbortWithError(http.StatusInternalServerError, err) + return + } + + ctx.Status(http.StatusNoContent) +} + +func DeleteAdmin(ctx *gin.Context) { + user := ctx.MustGet("user").(*op.User) + + if !user.IsRoot() { + ctx.AbortWithStatusJSON(http.StatusForbidden, model.NewApiErrorStringResp("permission denied")) + return + } + + req := model.IdReq{} + if err := model.Decode(ctx, &req); err != nil { + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) + return + } + + if err := user.SetRole(dbModel.RoleUser); err != nil { + ctx.AbortWithError(http.StatusInternalServerError, err) + return + } + + ctx.Status(http.StatusNoContent) +} diff --git a/server/handlers/init.go b/server/handlers/init.go index 591eb47..a5a6a7d 100644 --- a/server/handlers/init.go +++ b/server/handlers/init.go @@ -41,7 +41,7 @@ func Init(e *gin.Engine) { admin := api.Group("/admin") admin.Use(middlewares.AuthAdminMiddleware) - admin.GET("/settings", AdminSettings) + admin.GET("/settings/:group", AdminSettings) admin.POST("/settings", EditAdminSettings) } diff --git a/server/handlers/room.go b/server/handlers/room.go index c9fe28c..085a523 100644 --- a/server/handlers/room.go +++ b/server/handlers/room.go @@ -9,6 +9,7 @@ import ( "github.com/gin-gonic/gin" "github.com/synctv-org/synctv/internal/db" "github.com/synctv-org/synctv/internal/op" + "github.com/synctv-org/synctv/internal/setting" "github.com/synctv-org/synctv/server/middlewares" "github.com/synctv-org/synctv/server/model" "github.com/synctv-org/synctv/utils" @@ -31,7 +32,7 @@ func (e FormatErrNotSupportPosition) Error() string { func CreateRoom(ctx *gin.Context) { user := ctx.MustGet("user").(*op.User) - v, err := op.DisableCreateRoom.Get() + v, err := setting.DisableCreateRoom.Get() if err != nil { ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) return diff --git a/server/model/admin.go b/server/model/admin.go index 54cb3f5..385b651 100644 --- a/server/model/admin.go +++ b/server/model/admin.go @@ -1,10 +1,16 @@ package model import ( + "errors" + "github.com/gin-gonic/gin" json "github.com/json-iterator/go" ) +var ( + ErrInvalidID = errors.New("invalid id") +) + type AdminSettingsReq map[string]any func (asr *AdminSettingsReq) Validate() error {