From d3abf4bc45654c369beb850c2c96ef3f4b7f432e Mon Sep 17 00:00:00 2001 From: zijiren233 Date: Tue, 24 Oct 2023 13:36:46 +0800 Subject: [PATCH] Feat: cmd user manager --- cmd/admin/add.go | 48 ++++++++++++++++++++++++++ cmd/admin/admin.go | 11 ++++++ cmd/admin/delete.go | 48 ++++++++++++++++++++++++++ cmd/admin/show.go | 33 ++++++++++++++++++ cmd/root.go | 13 +++++-- cmd/user/ban.go | 49 ++++++++++++++++++++++++++ cmd/user/delete.go | 44 ++++++++++++++++++++++++ cmd/user/search.go | 44 ++++++++++++++++++++++++ cmd/user/unban.go | 49 ++++++++++++++++++++++++++ cmd/user/user.go | 9 +++++ internal/bootstrap/log.go | 8 +++++ internal/db/db.go | 2 +- internal/db/setting.go | 42 +++++++++++++++++++++++ internal/db/user.go | 70 +++++++++++++++++++++++++++++++++++--- internal/model/setting.go | 6 ++++ internal/model/user.go | 14 +++++++- internal/op/user.go | 3 ++ server/middlewares/auth.go | 13 +++++++ 18 files changed, 497 insertions(+), 9 deletions(-) create mode 100644 cmd/admin/add.go create mode 100644 cmd/admin/admin.go create mode 100644 cmd/admin/delete.go create mode 100644 cmd/admin/show.go create mode 100644 cmd/user/ban.go create mode 100644 cmd/user/delete.go create mode 100644 cmd/user/search.go create mode 100644 cmd/user/unban.go create mode 100644 cmd/user/user.go create mode 100644 internal/db/setting.go create mode 100644 internal/model/setting.go diff --git a/cmd/admin/add.go b/cmd/admin/add.go new file mode 100644 index 0000000..77235d1 --- /dev/null +++ b/cmd/admin/add.go @@ -0,0 +1,48 @@ +package admin + +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 admin by user id", + Long: `add admin 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.AddAdmin(u); err != nil { + fmt.Printf("add admin failed: %s", err) + return nil + } + fmt.Printf("add admin success: %s\n", u.Username) + return nil + }, +} + +func init() { + AdminCmd.AddCommand(AddCmd) +} diff --git a/cmd/admin/admin.go b/cmd/admin/admin.go new file mode 100644 index 0000000..73ae692 --- /dev/null +++ b/cmd/admin/admin.go @@ -0,0 +1,11 @@ +package admin + +import ( + "github.com/spf13/cobra" +) + +var AdminCmd = &cobra.Command{ + Use: "admin", + Short: "admin", + Long: `you must first shut down the server, otherwise the changes will not take effect.`, +} diff --git a/cmd/admin/delete.go b/cmd/admin/delete.go new file mode 100644 index 0000000..034420f --- /dev/null +++ b/cmd/admin/delete.go @@ -0,0 +1,48 @@ +package admin + +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 admin`, + 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.RemoveAdmin(u); err != nil { + fmt.Printf("remove admin failed: %s", err) + return nil + } + fmt.Printf("remove admin success: %s\n", u.Username) + return nil + }, +} + +func init() { + AdminCmd.AddCommand(RemoveCmd) +} diff --git a/cmd/admin/show.go b/cmd/admin/show.go new file mode 100644 index 0000000..ae294ea --- /dev/null +++ b/cmd/admin/show.go @@ -0,0 +1,33 @@ +package admin + +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 admin", + Long: `show admin`, + 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 { + admins := db.GetAdmins() + for _, admin := range admins { + fmt.Printf("id: %d\tusername: %s\n", admin.ID, admin.Username) + } + return nil + }, +} + +func init() { + AdminCmd.AddCommand(ShowCmd) +} diff --git a/cmd/root.go b/cmd/root.go index 4fde36c..4695f84 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -7,14 +7,16 @@ import ( "github.com/mitchellh/go-homedir" "github.com/spf13/cobra" + "github.com/synctv-org/synctv/cmd/admin" "github.com/synctv-org/synctv/cmd/flags" + "github.com/synctv-org/synctv/cmd/user" "github.com/synctv-org/synctv/internal/version" ) var RootCmd = &cobra.Command{ - Use: "synctv-server", - Short: "synctv-server", - Long: `synctv-server https://github.com/synctv-org/synctv`, + Use: "synctv", + Short: "synctv", + Long: `synctv https://github.com/synctv-org/synctv`, } func Execute() { @@ -38,3 +40,8 @@ func init() { RootCmd.PersistentFlags().StringVar(&flags.DataDir, "data-dir", filepath.Join(home, ".synctv"), "data dir") RootCmd.PersistentFlags().StringVarP(&flags.ConfigFile, "config", "f", "", "config file path") } + +func init() { + RootCmd.AddCommand(admin.AdminCmd) + RootCmd.AddCommand(user.UserCmd) +} diff --git a/cmd/user/ban.go b/cmd/user/ban.go new file mode 100644 index 0000000..22f34c0 --- /dev/null +++ b/cmd/user/ban.go @@ -0,0 +1,49 @@ +package user + +import ( + "errors" + "fmt" + "strconv" + + "github.com/spf13/cobra" + "github.com/synctv-org/synctv/internal/bootstrap" + "github.com/synctv-org/synctv/internal/db" +) + +var BanCmd = &cobra.Command{ + Use: "ban", + Short: "ban user with user id", + Long: "ban user with 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\n", err) + return nil + } + err = db.BanUser(u) + if err != nil { + fmt.Printf("ban user failed: %s\n", err) + return nil + } + fmt.Printf("ban user success: %s\n", u.Username) + return nil + }, +} + +func init() { + UserCmd.AddCommand(BanCmd) +} diff --git a/cmd/user/delete.go b/cmd/user/delete.go new file mode 100644 index 0000000..d71d7ae --- /dev/null +++ b/cmd/user/delete.go @@ -0,0 +1,44 @@ +package user + +import ( + "errors" + "fmt" + "strconv" + + "github.com/spf13/cobra" + "github.com/synctv-org/synctv/internal/bootstrap" + "github.com/synctv-org/synctv/internal/db" +) + +var DeleteCmd = &cobra.Command{ + Use: "delete", + Short: "delete", + Long: `delete user`, + 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.LoadAndDeleteUserByID(uint(id)) + if err != nil { + fmt.Printf("delete user failed: %s\n", err) + return nil + } + fmt.Printf("delete user success: %s\n", u.Username) + return nil + }, +} + +func init() { + UserCmd.AddCommand(DeleteCmd) +} diff --git a/cmd/user/search.go b/cmd/user/search.go new file mode 100644 index 0000000..dc9a41c --- /dev/null +++ b/cmd/user/search.go @@ -0,0 +1,44 @@ +package user + +import ( + "errors" + "fmt" + + "github.com/spf13/cobra" + "github.com/synctv-org/synctv/internal/bootstrap" + "github.com/synctv-org/synctv/internal/db" +) + +var SearchCmd = &cobra.Command{ + Use: "search", + Short: "search user by id or username", + Long: `search user by id or username`, + 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 or username") + } + us, err := db.GetUserByIDOrUsernameLike(args[0]) + if err != nil { + return err + } + if len(us) == 0 { + fmt.Println("user not found") + return nil + } + for _, u := range us { + fmt.Printf("id: %d\tusername: %s\tcreated_at: %s\trole: %s\n", u.ID, u.Username, u.CreatedAt, u.Role) + } + return nil + }, +} + +func init() { + UserCmd.AddCommand(SearchCmd) +} diff --git a/cmd/user/unban.go b/cmd/user/unban.go new file mode 100644 index 0000000..8250a55 --- /dev/null +++ b/cmd/user/unban.go @@ -0,0 +1,49 @@ +package user + +import ( + "errors" + "fmt" + "strconv" + + "github.com/spf13/cobra" + "github.com/synctv-org/synctv/internal/bootstrap" + "github.com/synctv-org/synctv/internal/db" +) + +var UnbanCmd = &cobra.Command{ + Use: "unban", + Short: "unban user with user id", + Long: "unban user with 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\n", err) + return nil + } + err = db.UnbanUser(u) + if err != nil { + fmt.Printf("unban user failed: %s", err) + return nil + } + fmt.Printf("unban user success: %s\n", u.Username) + return nil + }, +} + +func init() { + UserCmd.AddCommand(UnbanCmd) +} diff --git a/cmd/user/user.go b/cmd/user/user.go new file mode 100644 index 0000000..a6e9a7c --- /dev/null +++ b/cmd/user/user.go @@ -0,0 +1,9 @@ +package user + +import "github.com/spf13/cobra" + +var UserCmd = &cobra.Command{ + Use: "user", + Short: "user", + Long: `you must first shut down the server, otherwise the changes will not take effect.`, +} diff --git a/internal/bootstrap/log.go b/internal/bootstrap/log.go index 91c07d5..8967143 100644 --- a/internal/bootstrap/log.go +++ b/internal/bootstrap/log.go @@ -67,6 +67,14 @@ func InitLog(ctx context.Context) error { } func InitStdLog(ctx context.Context) error { + logrus.StandardLogger().SetOutput(os.Stdout) + log.SetOutput(os.Stdout) setLog(logrus.StandardLogger()) return nil } + +func InitDiscardLog(ctx context.Context) error { + logrus.StandardLogger().SetOutput(io.Discard) + log.SetOutput(io.Discard) + return nil +} diff --git a/internal/db/db.go b/internal/db/db.go index 10d3eea..9e7253a 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -19,7 +19,7 @@ var ( func Init(d *gorm.DB, t conf.DatabaseType) error { db = d dbType = t - return AutoMigrate(new(model.Movie), new(model.Room), new(model.User), new(model.RoomUserRelation), new(model.UserProvider)) + return AutoMigrate(new(model.Movie), new(model.Room), new(model.User), new(model.RoomUserRelation), new(model.UserProvider), new(model.SettingItem)) } func AutoMigrate(dst ...any) error { diff --git a/internal/db/setting.go b/internal/db/setting.go new file mode 100644 index 0000000..90baefd --- /dev/null +++ b/internal/db/setting.go @@ -0,0 +1,42 @@ +package db + +import ( + "github.com/synctv-org/synctv/internal/model" + "gorm.io/gorm/clause" +) + +func GetSettingItems() ([]*model.SettingItem, error) { + var items []*model.SettingItem + err := db.Find(&items).Error + return items, err +} + +func GetSettingItemByName(name string) (*model.SettingItem, error) { + var item model.SettingItem + err := db.Where("name = ?", name).First(&item).Error + return &item, err +} + +func SaveSettingItem(item *model.SettingItem) error { + return db.Clauses(clause.OnConflict{ + UpdateAll: true, + }).Save(item).Error +} + +func DeleteSettingItem(item *model.SettingItem) error { + return db.Delete(item).Error +} + +func DeleteSettingItemByName(name string) error { + return db.Where("name = ?", name).Delete(&model.SettingItem{}).Error +} + +func GetSettingItemValue(name string) (string, error) { + var value string + err := db.Model(&model.SettingItem{}).Where("name = ?", name).Select("value").First(&value).Error + return value, err +} + +func SetSettingItemValue(name, value string) error { + return db.Model(&model.SettingItem{}).Where("name = ?", name).Update("value", value).Error +} diff --git a/internal/db/user.go b/internal/db/user.go index a86f3c6..075f5b5 100644 --- a/internal/db/user.go +++ b/internal/db/user.go @@ -105,6 +105,15 @@ func GerUsersIDByUsernameLike(username string, scopes ...func(*gorm.DB) *gorm.DB return ids } +func GetUserByIDOrUsernameLike(idOrUsername string, scopes ...func(*gorm.DB) *gorm.DB) ([]*model.User, error) { + var users []*model.User + err := db.Where("id = ? OR username LIKE ?", idOrUsername, fmt.Sprintf("%%%s%%", idOrUsername)).Scopes(scopes...).Find(&users).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return users, errors.New("user not found") + } + return users, err +} + func GetUserByID(id uint) (*model.User, error) { u := &model.User{} err := db.Where("id = ?", id).First(u).Error @@ -123,6 +132,38 @@ func GetUsersByRoomID(roomID uint, scopes ...func(*gorm.DB) *gorm.DB) ([]model.U return users, err } +func BanUser(u *model.User) error { + if u.Role == model.RoleBanned { + return nil + } + u.Role = model.RoleBanned + return SaveUser(u) +} + +func BanUserByID(userID uint) error { + err := db.Model(&model.User{}).Where("id = ?", userID).Update("role", model.RoleBanned).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("user not found") + } + return err +} + +func UnbanUser(u *model.User) error { + if u.Role != model.RoleBanned { + return errors.New("user is not banned") + } + u.Role = model.RoleUser + return SaveUser(u) +} + +func UnbanUserByID(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 DeleteUserByID(userID uint) error { err := db.Unscoped().Delete(&model.User{}, userID).Error if errors.Is(err, gorm.ErrRecordNotFound) { @@ -133,14 +174,13 @@ func DeleteUserByID(userID uint) error { func LoadAndDeleteUserByID(userID uint, columns ...clause.Column) (*model.User, error) { u := &model.User{} - err := db.Unscoped(). + if db.Unscoped(). Clauses(clause.Returning{Columns: columns}). Delete(u, userID). - Error - if errors.Is(err, gorm.ErrRecordNotFound) { + RowsAffected == 0 { return u, errors.New("user not found") } - return u, err + return u, nil } func DeleteUserByUsername(username string) error { @@ -183,3 +223,25 @@ func SetUserHashedPassword(userID uint, hashedPassword []byte) error { func SaveUser(u *model.User) error { return db.Save(u).Error } + +func AddAdmin(u *model.User) error { + if u.Role >= model.RoleAdmin { + return nil + } + u.Role = model.RoleAdmin + return SaveUser(u) +} + +func RemoveAdmin(u *model.User) error { + if u.Role < model.RoleAdmin { + return nil + } + u.Role = model.RoleUser + return SaveUser(u) +} + +func GetAdmins() []*model.User { + var users []*model.User + db.Where("role >= ?", model.RoleAdmin).Find(&users) + return users +} diff --git a/internal/model/setting.go b/internal/model/setting.go new file mode 100644 index 0000000..5dc69e2 --- /dev/null +++ b/internal/model/setting.go @@ -0,0 +1,6 @@ +package model + +type SettingItem struct { + Name string `gorm:"primaryKey"` + Value string +} diff --git a/internal/model/user.go b/internal/model/user.go index 8558513..7998086 100644 --- a/internal/model/user.go +++ b/internal/model/user.go @@ -14,9 +14,21 @@ const ( RoleBanned Role = iota RoleUser RoleAdmin - RoleRoot ) +func (r Role) String() string { + switch r { + case RoleBanned: + return "banned" + case RoleUser: + return "user" + case RoleAdmin: + return "admin" + default: + return "unknown" + } +} + type User struct { ID uint `gorm:"primarykey"` CreatedAt time.Time diff --git a/internal/op/user.go b/internal/op/user.go index fb63f0e..9b7da6d 100644 --- a/internal/op/user.go +++ b/internal/op/user.go @@ -23,6 +23,9 @@ func (u *User) NewMovie(movie model.MovieInfo) model.Movie { } func (u *User) HasPermission(roomID uint, permission model.Permission) bool { + if u.Role == model.RoleAdmin { + return true + } ur, err := db.GetRoomUserRelation(roomID, u.ID) if err != nil { return false diff --git a/server/middlewares/auth.go b/server/middlewares/auth.go index 11b4c4b..7413a08 100644 --- a/server/middlewares/auth.go +++ b/server/middlewares/auth.go @@ -8,6 +8,7 @@ import ( "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" "github.com/synctv-org/synctv/internal/conf" + dbModel "github.com/synctv-org/synctv/internal/model" "github.com/synctv-org/synctv/internal/op" "github.com/synctv-org/synctv/server/model" "github.com/zijiren233/stream" @@ -75,6 +76,9 @@ func AuthRoom(Authorization string) (*op.User, *op.Room, error) { if err != nil { return nil, nil, err } + if u.Role == dbModel.RoleBanned { + return nil, nil, errors.New("user banned") + } r, err := op.LoadOrInitRoomByID(claims.RoomId) if err != nil { @@ -101,11 +105,17 @@ func AuthUser(Authorization string) (*op.User, error) { if err != nil { return nil, err } + if u.Role == dbModel.RoleBanned { + return nil, errors.New("user banned") + } return u, nil } func NewAuthUserToken(user *op.User) (string, error) { + if user.Role == dbModel.RoleBanned { + return "", errors.New("user banned") + } t, err := time.ParseDuration(conf.Conf.Jwt.Expire) if err != nil { return "", err @@ -121,6 +131,9 @@ func NewAuthUserToken(user *op.User) (string, error) { } func NewAuthRoomToken(user *op.User, room *op.Room) (string, error) { + if user.Role == dbModel.RoleBanned { + return "", errors.New("user banned") + } t, err := time.ParseDuration(conf.Conf.Jwt.Expire) if err != nil { return "", err