From b596d9ea0335ac371524e2b25143ffe04b539955 Mon Sep 17 00:00:00 2001 From: zijiren233 Date: Fri, 27 Oct 2023 18:02:12 +0800 Subject: [PATCH] Feat: pending user --- internal/db/user.go | 4 +- internal/model/user.go | 28 +++------- internal/op/user.go | 4 ++ internal/settings/var.go | 1 + server/handlers/admin.go | 103 ++++++++++++++++++++++++++++++++++++- server/middlewares/auth.go | 12 +++++ server/model/user.go | 15 ++++++ server/oauth2/auth.go | 14 ++++- 8 files changed, 156 insertions(+), 25 deletions(-) diff --git a/internal/db/user.go b/internal/db/user.go index 2804f80..aa1f26e 100644 --- a/internal/db/user.go +++ b/internal/db/user.go @@ -283,9 +283,9 @@ func GetAllUserWithRoleUser(role model.Role, scopes ...func(*gorm.DB) *gorm.DB) return users } -func GetAllUserWithRoleUserCount(scopes ...func(*gorm.DB) *gorm.DB) int64 { +func GetAllUserCountWithRole(role model.Role, scopes ...func(*gorm.DB) *gorm.DB) int64 { var count int64 - db.Model(&model.User{}).Where("role = ?", model.RoleUser).Scopes(scopes...).Count(&count) + db.Model(&model.User{}).Where("role = ?", role).Scopes(scopes...).Count(&count) return count } diff --git a/internal/model/user.go b/internal/model/user.go index 4e56904..94c4541 100644 --- a/internal/model/user.go +++ b/internal/model/user.go @@ -8,37 +8,23 @@ import ( "gorm.io/gorm" ) -type Role uint8 +type Role string const ( - RoleBanned Role = iota - RoleUser - RoleAdmin - RoleRoot + RoleBanned Role = "banned" + RolePending Role = "pending" + RoleUser Role = "user" + RoleAdmin Role = "admin" + RoleRoot Role = "root" ) -func (r Role) String() string { - switch r { - case RoleBanned: - return "banned" - case RoleUser: - return "user" - case RoleAdmin: - return "admin" - case RoleRoot: - return "root" - default: - return "unknown" - } -} - type User struct { ID uint `gorm:"primarykey"` CreatedAt time.Time UpdatedAt time.Time Providers []UserProvider `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` Username string `gorm:"not null;uniqueIndex"` - Role Role `gorm:"not null"` + Role Role `gorm:"not null;default:user"` GroupUserRelations []RoomUserRelation `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` Rooms []Room `gorm:"foreignKey:CreatorID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` Movies []Movie `gorm:"foreignKey:CreatorID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL"` diff --git a/internal/op/user.go b/internal/op/user.go index 1e6a489..235a58a 100644 --- a/internal/op/user.go +++ b/internal/op/user.go @@ -34,6 +34,10 @@ func (u *User) IsBanned() bool { return u.Role == model.RoleBanned } +func (u *User) IsPending() bool { + return u.Role == model.RolePending +} + func (u *User) HasPermission(roomID uint, permission model.Permission) bool { if u.Role >= model.RoleAdmin { return true diff --git a/internal/settings/var.go b/internal/settings/var.go index 374626f..ec06358 100644 --- a/internal/settings/var.go +++ b/internal/settings/var.go @@ -8,4 +8,5 @@ var ( var ( DisableUserSignup = newBoolSetting("disable_user_signup", false, model.SettingGroupUser) + SignupNeedReview = newBoolSetting("signup_need_review", false, model.SettingGroupUser) ) diff --git a/server/handlers/admin.go b/server/handlers/admin.go index 7b04b70..9bf34aa 100644 --- a/server/handlers/admin.go +++ b/server/handlers/admin.go @@ -1,11 +1,13 @@ package handlers import ( + "errors" "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" @@ -104,7 +106,7 @@ func Users(ctx *gin.Context) { } ctx.JSON(http.StatusOK, model.NewApiDataResp(gin.H{ - "total": db.GetAllUserWithRoleUserCount(scopes...), + "total": db.GetAllUserCountWithRole(dbModel.RoleUser, scopes...), "list": genUserListResp(dbModel.RoleUser, append(scopes, db.Paginate(page, pageSize))...), })) } @@ -122,3 +124,102 @@ func genUserListResp(role dbModel.Role, scopes ...func(db *gorm.DB) *gorm.DB) [] } return resp } + +func PendingUsers(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 + } + + page, pageSize, err := GetPageAndPageSize(ctx) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) + return + } + + var desc = ctx.DefaultQuery("sort", "desc") == "desc" + + scopes := []func(db *gorm.DB) *gorm.DB{} + + if keyword := ctx.Query("keyword"); keyword != "" { + scopes = append(scopes, db.WhereUserNameLike(keyword)) + } + + 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 + } + + ctx.JSON(http.StatusOK, model.NewApiDataResp(gin.H{ + "total": db.GetAllUserCountWithRole(dbModel.RolePending, scopes...), + "list": genUserListResp(dbModel.RolePending, append(scopes, db.Paginate(page, pageSize))...), + })) +} + +func ApprovePendingUser(Authorization string, userID uint) error { + user, err := op.GetUserById(userID) + if err != nil { + return err + } + if !user.IsPending() { + return errors.New("user is not pending") + } + if err := user.SetRole(dbModel.RoleUser); err != nil { + return err + } + return nil +} + +func BanUser(ctx *gin.Context) { + user := ctx.MustGet("user").(*op.User) + + req := model.UserIDReq{} + if err := model.Decode(ctx, &req); err != nil { + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) + return + } + + u, err := op.GetUserById(req.ID) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) + return + } + + if u.ID == user.ID { + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("cannot ban yourself")) + return + } + if u.IsRoot() { + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("cannot ban root user")) + return + } + + err = u.SetRole(dbModel.RoleBanned) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + + ctx.Status(http.StatusNoContent) +} diff --git a/server/middlewares/auth.go b/server/middlewares/auth.go index 6611898..9a68898 100644 --- a/server/middlewares/auth.go +++ b/server/middlewares/auth.go @@ -79,6 +79,9 @@ func AuthRoom(Authorization string) (*op.User, *op.Room, error) { if u.IsBanned() { return nil, nil, errors.New("user banned") } + if u.IsPending() { + return nil, nil, errors.New("user is pending, need admin to approve") + } r, err := op.LoadOrInitRoomByID(claims.RoomId) if err != nil { @@ -108,6 +111,9 @@ func AuthUser(Authorization string) (*op.User, error) { if u.IsBanned() { return nil, errors.New("user banned") } + if u.IsPending() { + return nil, errors.New("user is pending, need admin to approve") + } return u, nil } @@ -138,6 +144,9 @@ func NewAuthUserToken(user *op.User) (string, error) { if user.IsBanned() { return "", errors.New("user banned") } + if user.IsPending() { + return "", errors.New("user is pending, need admin to approve") + } t, err := time.ParseDuration(conf.Conf.Jwt.Expire) if err != nil { return "", err @@ -156,6 +165,9 @@ func NewAuthRoomToken(user *op.User, room *op.Room) (string, error) { if user.IsBanned() { return "", errors.New("user banned") } + if user.IsPending() { + return "", errors.New("user is pending, need admin to approve") + } t, err := time.ParseDuration(conf.Conf.Jwt.Expire) if err != nil { return "", err diff --git a/server/model/user.go b/server/model/user.go index 5fc1ded..ddac9cb 100644 --- a/server/model/user.go +++ b/server/model/user.go @@ -76,3 +76,18 @@ func (s *SetUsernameReq) Validate() error { func (s *SetUsernameReq) Decode(ctx *gin.Context) error { return json.NewDecoder(ctx.Request.Body).Decode(s) } + +type UserIDReq struct { + ID uint `json:"id"` +} + +func (u *UserIDReq) Decode(ctx *gin.Context) error { + return json.NewDecoder(ctx.Request.Body).Decode(u) +} + +func (u *UserIDReq) Validate() error { + if u.ID == 0 { + return errors.New("id is required") + } + return nil +} diff --git a/server/oauth2/auth.go b/server/oauth2/auth.go index a2b3370..3a1e628 100644 --- a/server/oauth2/auth.go +++ b/server/oauth2/auth.go @@ -5,6 +5,8 @@ import ( "time" "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/provider" "github.com/synctv-org/synctv/internal/provider/providers" @@ -93,7 +95,17 @@ func OAuth2Callback(ctx *gin.Context) { if disable { user, err = op.GetUserByProvider(p, ui.ProviderUserID) } else { - user, err = op.CreateOrLoadUser(ui.Username, p, ui.ProviderUserID) + var review bool + review, err = settings.SignupNeedReview.Get() + if err != nil { + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + if review { + user, err = op.CreateOrLoadUser(ui.Username, p, ui.ProviderUserID, db.WithRole(dbModel.RolePending)) + } else { + user, err = op.CreateOrLoadUser(ui.Username, p, ui.ProviderUserID) + } } if err != nil { ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))