From eb66cbe655e4bdd8a15b574cd191da5432686764 Mon Sep 17 00:00:00 2001 From: zijiren233 Date: Wed, 25 Oct 2023 17:19:57 +0800 Subject: [PATCH] Feat: root and admin role api --- cmd/root.go | 2 + cmd/root/add.go | 48 ++++++++++++++++++++ cmd/root/delete.go | 48 ++++++++++++++++++++ cmd/root/root.go | 9 ++++ cmd/root/show.go | 33 ++++++++++++++ internal/db/db.go | 11 +++++ internal/db/user.go | 14 +++++- internal/op/users.go | 9 ++++ server/handlers/admin.go | 79 +++++++++++++++++++++------------ server/handlers/init.go | 18 +++++++- server/handlers/room.go | 15 +++---- server/handlers/root.go | 89 ++++++++++++++++++++++++++++++++++++++ server/handlers/user.go | 5 +-- server/middlewares/auth.go | 22 ++++++++++ server/model/user.go | 8 ++++ 15 files changed, 367 insertions(+), 43 deletions(-) create mode 100644 cmd/root/add.go create mode 100644 cmd/root/delete.go create mode 100644 cmd/root/root.go create mode 100644 cmd/root/show.go create mode 100644 server/handlers/root.go diff --git a/cmd/root.go b/cmd/root.go index 8e963b6..afacc2c 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -9,6 +9,7 @@ import ( "github.com/spf13/cobra" "github.com/synctv-org/synctv/cmd/admin" "github.com/synctv-org/synctv/cmd/flags" + "github.com/synctv-org/synctv/cmd/root" "github.com/synctv-org/synctv/cmd/setting" "github.com/synctv-org/synctv/cmd/user" "github.com/synctv-org/synctv/internal/version" @@ -46,4 +47,5 @@ func init() { RootCmd.AddCommand(admin.AdminCmd) RootCmd.AddCommand(user.UserCmd) RootCmd.AddCommand(setting.SettingCmd) + RootCmd.AddCommand(root.RootCmd) } diff --git a/cmd/root/add.go b/cmd/root/add.go new file mode 100644 index 0000000..ff24fac --- /dev/null +++ b/cmd/root/add.go @@ -0,0 +1,48 @@ +package root + +import ( + "errors" + "fmt" + "strconv" + + "github.com/spf13/cobra" + "github.com/synctv-org/synctv/internal/bootstrap" + "github.com/synctv-org/synctv/internal/db" +) + +var AddCmd = &cobra.Command{ + Use: "add", + Short: "add root by user id", + Long: `add root by user id`, + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + return bootstrap.New(bootstrap.WithContext(cmd.Context())).Add( + bootstrap.InitDiscardLog, + bootstrap.InitConfig, + bootstrap.InitDatabase, + ).Run() + }, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.New("missing user id") + } + id, err := strconv.Atoi(args[0]) + if err != nil { + return fmt.Errorf("invalid user id: %s", args[0]) + } + u, err := db.GetUserByID(uint(id)) + if err != nil { + fmt.Printf("get user failed: %s", err) + return nil + } + if err := db.AddRoot(u); err != nil { + fmt.Printf("add root failed: %s", err) + return nil + } + fmt.Printf("add root success: %s\n", u.Username) + return nil + }, +} + +func init() { + RootCmd.AddCommand(AddCmd) +} diff --git a/cmd/root/delete.go b/cmd/root/delete.go new file mode 100644 index 0000000..7a068b7 --- /dev/null +++ b/cmd/root/delete.go @@ -0,0 +1,48 @@ +package root + +import ( + "errors" + "fmt" + "strconv" + + "github.com/spf13/cobra" + "github.com/synctv-org/synctv/internal/bootstrap" + "github.com/synctv-org/synctv/internal/db" +) + +var RemoveCmd = &cobra.Command{ + Use: "remove", + Short: "remove", + Long: `remove root`, + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + return bootstrap.New(bootstrap.WithContext(cmd.Context())).Add( + bootstrap.InitDiscardLog, + bootstrap.InitConfig, + bootstrap.InitDatabase, + ).Run() + }, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.New("missing user id") + } + id, err := strconv.Atoi(args[0]) + if err != nil { + return fmt.Errorf("invalid user id: %s", args[0]) + } + u, err := db.GetUserByID(uint(id)) + if err != nil { + fmt.Printf("get user failed: %s", err) + return nil + } + if err := db.RemoveRoot(u); err != nil { + fmt.Printf("remove root failed: %s", err) + return nil + } + fmt.Printf("remove root success: %s\n", u.Username) + return nil + }, +} + +func init() { + RootCmd.AddCommand(RemoveCmd) +} diff --git a/cmd/root/root.go b/cmd/root/root.go new file mode 100644 index 0000000..1e061a9 --- /dev/null +++ b/cmd/root/root.go @@ -0,0 +1,9 @@ +package root + +import "github.com/spf13/cobra" + +var RootCmd = &cobra.Command{ + Use: "root", + Short: "root", + Long: `you must first shut down the server, otherwise the changes will not take effect.`, +} diff --git a/cmd/root/show.go b/cmd/root/show.go new file mode 100644 index 0000000..b943f5b --- /dev/null +++ b/cmd/root/show.go @@ -0,0 +1,33 @@ +package root + +import ( + "fmt" + + "github.com/spf13/cobra" + "github.com/synctv-org/synctv/internal/bootstrap" + "github.com/synctv-org/synctv/internal/db" +) + +var ShowCmd = &cobra.Command{ + Use: "show", + Short: "show root", + Long: `show root`, + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + return bootstrap.New(bootstrap.WithContext(cmd.Context())).Add( + bootstrap.InitDiscardLog, + bootstrap.InitConfig, + bootstrap.InitDatabase, + ).Run() + }, + RunE: func(cmd *cobra.Command, args []string) error { + roots := db.GetRoots() + for _, root := range roots { + fmt.Printf("id: %d\tusername: %s\n", root.ID, root.Username) + } + return nil + }, +} + +func init() { + RootCmd.AddCommand(ShowCmd) +} diff --git a/internal/db/db.go b/internal/db/db.go index 4aa0d74..0373c4f 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -162,6 +162,17 @@ func WhereRoomNameLike(name string) func(db *gorm.DB) *gorm.DB { } } +func WhereUserNameLike(name string) func(db *gorm.DB) *gorm.DB { + return func(db *gorm.DB) *gorm.DB { + switch dbType { + case conf.DatabaseTypePostgres: + return db.Where("username ILIKE ?", utils.LIKE(name)) + default: + return db.Where("username LIKE ?", utils.LIKE(name)) + } + } +} + func WhereCreatorIDIn(ids []uint) func(db *gorm.DB) *gorm.DB { return func(db *gorm.DB) *gorm.DB { return db.Where("creator_id IN ?", ids) diff --git a/internal/db/user.go b/internal/db/user.go index a9dd85c..f501924 100644 --- a/internal/db/user.go +++ b/internal/db/user.go @@ -245,7 +245,7 @@ func RemoveAdmin(u *model.User) error { func GetAdmins() []*model.User { var users []*model.User - db.Where("role >= ?", model.RoleAdmin).Find(&users) + db.Where("role == ?", model.RoleAdmin).Find(&users) return users } @@ -315,3 +315,15 @@ func SetRoleByID(userID uint, role model.Role) error { } return err } + +func GetAllUserWithRoleUser(role model.Role, scopes ...func(*gorm.DB) *gorm.DB) []*model.User { + users := []*model.User{} + db.Where("role = ?", role).Scopes(scopes...).Find(&users) + return users +} + +func GetAllUserWithRoleUserCount(scopes ...func(*gorm.DB) *gorm.DB) int64 { + var count int64 + db.Model(&model.User{}).Where("role = ?", model.RoleUser).Scopes(scopes...).Count(&count) + return count +} diff --git a/internal/op/users.go b/internal/op/users.go index 447bf74..01eb239 100644 --- a/internal/op/users.go +++ b/internal/op/users.go @@ -115,3 +115,12 @@ func GetUserName(userID uint) string { } return u.Username } + +func SetRoleByID(userID uint, role model.Role) error { + err := db.SetRoleByID(userID, role) + if err != nil { + return err + } + userCache.Remove(userID) + return nil +} diff --git a/server/handlers/admin.go b/server/handlers/admin.go index 756c9b6..7b04b70 100644 --- a/server/handlers/admin.go +++ b/server/handlers/admin.go @@ -4,10 +4,11 @@ import ( "net/http" "github.com/gin-gonic/gin" + "github.com/synctv-org/synctv/internal/db" dbModel "github.com/synctv-org/synctv/internal/model" - "github.com/synctv-org/synctv/internal/op" "github.com/synctv-org/synctv/internal/settings" "github.com/synctv-org/synctv/server/model" + "gorm.io/gorm" ) func EditAdminSettings(ctx *gin.Context) { @@ -56,46 +57,68 @@ 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")) +func Users(ctx *gin.Context) { + // user := ctx.MustGet("user").(*op.User) + order := ctx.Query("order") + if order == "" { + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("order is required")) return } - req := model.IdReq{} - if err := model.Decode(ctx, &req); err != nil { + page, pageSize, err := GetPageAndPageSize(ctx) + if 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) -} + var desc = ctx.DefaultQuery("sort", "desc") == "desc" -func DeleteAdmin(ctx *gin.Context) { - user := ctx.MustGet("user").(*op.User) + scopes := []func(db *gorm.DB) *gorm.DB{} - if !user.IsRoot() { - ctx.AbortWithStatusJSON(http.StatusForbidden, model.NewApiErrorStringResp("permission denied")) - return + if keyword := ctx.Query("keyword"); keyword != "" { + scopes = append(scopes, db.WhereUserNameLike(keyword)) } - req := model.IdReq{} - if err := model.Decode(ctx, &req); err != nil { - ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) + switch order { + case "createdAt": + if desc { + scopes = append(scopes, db.OrderByCreatedAtDesc) + } else { + scopes = append(scopes, db.OrderByCreatedAtAsc) + } + case "name": + if desc { + scopes = append(scopes, db.OrderByDesc("username")) + } else { + scopes = append(scopes, db.OrderByAsc("username")) + } + case "id": + if desc { + scopes = append(scopes, db.OrderByIDDesc) + } else { + scopes = append(scopes, db.OrderByIDAsc) + } + default: + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("not support order")) return } - if err := user.SetRole(dbModel.RoleUser); err != nil { - ctx.AbortWithError(http.StatusInternalServerError, err) - return - } + ctx.JSON(http.StatusOK, model.NewApiDataResp(gin.H{ + "total": db.GetAllUserWithRoleUserCount(scopes...), + "list": genUserListResp(dbModel.RoleUser, append(scopes, db.Paginate(page, pageSize))...), + })) +} - ctx.Status(http.StatusNoContent) +func genUserListResp(role dbModel.Role, scopes ...func(db *gorm.DB) *gorm.DB) []*model.UserInfoResp { + us := db.GetAllUserWithRoleUser(role, scopes...) + resp := make([]*model.UserInfoResp, len(us)) + for i, v := range us { + resp[i] = &model.UserInfoResp{ + ID: v.ID, + Username: v.Username, + Role: v.Role, + CreatedAt: v.CreatedAt.UnixMilli(), + } + } + return resp } diff --git a/server/handlers/init.go b/server/handlers/init.go index a5a6a7d..c2f4d83 100644 --- a/server/handlers/init.go +++ b/server/handlers/init.go @@ -39,11 +39,25 @@ func Init(e *gin.Engine) { { admin := api.Group("/admin") + root := api.Group("/admin") admin.Use(middlewares.AuthAdminMiddleware) + root.Use(middlewares.AuthRootMiddleware) - admin.GET("/settings/:group", AdminSettings) + { + admin.GET("/settings/:group", AdminSettings) + + admin.POST("/settings", EditAdminSettings) + + admin.GET("/users", Users) + } - admin.POST("/settings", EditAdminSettings) + { + root.GET("/admins", Admins) + + root.POST("/addAdmin", AddAdmin) + + root.POST("deleteAdmin", DeleteAdmin) + } } { diff --git a/server/handlers/room.go b/server/handlers/room.go index 8420977..675a919 100644 --- a/server/handlers/room.go +++ b/server/handlers/room.go @@ -114,7 +114,6 @@ func RoomList(ctx *gin.Context) { ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) return } - resp := make([]*model.RoomListResp, 0, pageSize) var desc = ctx.DefaultQuery("sort", "desc") == "desc" @@ -177,24 +176,24 @@ func RoomList(ctx *gin.Context) { return } - resp = genRoomListResp(resp, append(scopes, db.Paginate(page, pageSize))...) - ctx.JSON(http.StatusOK, model.NewApiDataResp(gin.H{ "total": db.GetAllRoomsWithoutHiddenCount(scopes...), - "list": resp, + "list": genRoomListResp(append(scopes, db.Paginate(page, pageSize))...), })) } -func genRoomListResp(resp []*model.RoomListResp, scopes ...func(db *gorm.DB) *gorm.DB) []*model.RoomListResp { - for _, r := range db.GetAllRoomsWithoutHidden(scopes...) { - resp = append(resp, &model.RoomListResp{ +func genRoomListResp(scopes ...func(db *gorm.DB) *gorm.DB) []*model.RoomListResp { + rs := db.GetAllRoomsWithoutHidden(scopes...) + resp := make([]*model.RoomListResp, len(rs)) + for i, r := range rs { + resp[i] = &model.RoomListResp{ RoomId: r.ID, RoomName: r.Name, PeopleNum: op.ClientNum(r.ID), NeedPassword: len(r.HashedPassword) != 0, Creator: op.GetUserName(r.CreatorID), CreatedAt: r.CreatedAt.UnixMilli(), - }) + } } return resp } diff --git a/server/handlers/root.go b/server/handlers/root.go new file mode 100644 index 0000000..6e41f59 --- /dev/null +++ b/server/handlers/root.go @@ -0,0 +1,89 @@ +package handlers + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/synctv-org/synctv/internal/db" + dbModel "github.com/synctv-org/synctv/internal/model" + "github.com/synctv-org/synctv/internal/op" + "github.com/synctv-org/synctv/server/model" +) + +func Admins(ctx *gin.Context) { + // user := ctx.MustGet("user").(*op.User) + + u := db.GetAdmins() + us := make([]model.UserInfoResp, len(u)) + for i, v := range u { + us[i] = model.UserInfoResp{ + ID: v.ID, + Username: v.Username, + Role: v.Role, + CreatedAt: v.CreatedAt.UnixMilli(), + } + } + ctx.JSON(http.StatusOK, model.NewApiDataResp(us)) +} + +func AddAdmin(ctx *gin.Context) { + user := ctx.MustGet("user").(*op.User) + + req := model.IdReq{} + if err := model.Decode(ctx, &req); err != nil { + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) + return + } + + if req.Id == user.ID { + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("cannot add yourself")) + return + } + u, err := op.GetUserById(req.Id) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorStringResp("user not found")) + return + } + if u.Role >= dbModel.RoleAdmin { + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("user is already admin")) + return + } + + if err := u.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) + + req := model.IdReq{} + if err := model.Decode(ctx, &req); err != nil { + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) + return + } + + if req.Id == user.ID { + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("cannot remove yourself")) + return + } + u, err := op.GetUserById(req.Id) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorStringResp("user not found")) + return + } + if u.Role == dbModel.RoleRoot { + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("cannot remove root")) + return + } + + if err := u.SetRole(dbModel.RoleUser); err != nil { + ctx.AbortWithError(http.StatusInternalServerError, err) + return + } + + ctx.Status(http.StatusNoContent) +} diff --git a/server/handlers/user.go b/server/handlers/user.go index f042140..6cc2ab2 100644 --- a/server/handlers/user.go +++ b/server/handlers/user.go @@ -42,7 +42,6 @@ func UserRooms(ctx *gin.Context) { ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) return } - resp := make([]*model.RoomListResp, 0, pageSize) var desc = ctx.DefaultQuery("sort", "desc") == "desc" @@ -101,10 +100,8 @@ func UserRooms(ctx *gin.Context) { return } - resp = genRoomListResp(resp, append(scopes, db.Paginate(page, pageSize))...) - ctx.JSON(http.StatusOK, model.NewApiDataResp(gin.H{ "total": db.GetAllRoomsWithoutHiddenCount(scopes...), - "list": resp, + "list": genRoomListResp(append(scopes, db.Paginate(page, pageSize))...), })) } diff --git a/server/middlewares/auth.go b/server/middlewares/auth.go index 61b952e..6611898 100644 --- a/server/middlewares/auth.go +++ b/server/middlewares/auth.go @@ -123,6 +123,17 @@ func AuthAdmin(Authorization string) (*op.User, error) { return u, nil } +func AuthRoot(Authorization string) (*op.User, error) { + u, err := AuthUser(Authorization) + if err != nil { + return nil, err + } + if !u.IsRoot() { + return nil, errors.New("user is not admin") + } + return u, nil +} + func NewAuthUserToken(user *op.User) (string, error) { if user.IsBanned() { return "", errors.New("user banned") @@ -196,3 +207,14 @@ func AuthAdminMiddleware(ctx *gin.Context) { ctx.Set("user", user) ctx.Next() } + +func AuthRootMiddleware(ctx *gin.Context) { + user, err := AuthRoot(ctx.GetHeader("Authorization")) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusForbidden, model.NewApiErrorResp(err)) + return + } + + ctx.Set("user", user) + ctx.Next() +} diff --git a/server/model/user.go b/server/model/user.go index 7c5bff8..ab36f89 100644 --- a/server/model/user.go +++ b/server/model/user.go @@ -5,6 +5,7 @@ import ( "github.com/gin-gonic/gin" json "github.com/json-iterator/go" + dbModel "github.com/synctv-org/synctv/internal/model" ) type SetUserPasswordReq struct { @@ -77,3 +78,10 @@ func (s *SignupUserReq) Validate() error { } return nil } + +type UserInfoResp struct { + ID uint `json:"id"` + Username string `json:"username"` + Role dbModel.Role `json:"role"` + CreatedAt int64 `json:"createdAt"` +}