diff --git a/internal/db/db.go b/internal/db/db.go index ff62249..44ac0a3 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -184,3 +184,9 @@ func Select(columns ...string) func(db *gorm.DB) *gorm.DB { return db.Select(columns) } } + +func WhereStatus(status model.RoomStatus) func(db *gorm.DB) *gorm.DB { + return func(db *gorm.DB) *gorm.DB { + return db.Where("status = ?", status) + } +} diff --git a/internal/db/room.go b/internal/db/room.go index 469ecf7..3f97570 100644 --- a/internal/db/room.go +++ b/internal/db/room.go @@ -195,3 +195,11 @@ func GetAllRoomsByUserID(userID string) []*model.Room { db.Where("creator_id = ?", userID).Find(&rooms) return rooms } + +func SetRoomStatus(roomID string, status model.RoomStatus) error { + err := db.Model(&model.Room{}).Where("id = ?", roomID).Update("status", status).Error + if err != nil && errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("room not found") + } + return err +} diff --git a/internal/model/room.go b/internal/model/room.go index 0293338..f9ef5d1 100644 --- a/internal/model/room.go +++ b/internal/model/room.go @@ -49,3 +49,19 @@ func (r *Room) NeedPassword() bool { func (r *Room) CheckPassword(password string) bool { return !r.NeedPassword() || bcrypt.CompareHashAndPassword(r.HashedPassword, stream.StringToBytes(password)) == nil } + +func (r *Room) IsBanned() bool { + return r.Status == RoomStatusBanned +} + +func (r *Room) IsPending() bool { + return r.Status == RoomStatusPending +} + +func (r *Room) IsStopped() bool { + return r.Status == RoomStatusStopped +} + +func (r *Room) IsActive() bool { + return r.Status == RoomStatusActive +} diff --git a/internal/model/user.go b/internal/model/user.go index d6ec735..132df17 100644 --- a/internal/model/user.go +++ b/internal/model/user.go @@ -49,7 +49,11 @@ func (u *User) IsRoot() bool { } func (u *User) IsAdmin() bool { - return u.Role == RoleAdmin + return u.Role == RoleAdmin || u.IsRoot() +} + +func (u *User) IsPending() bool { + return u.Role == RolePending } func (u *User) IsBanned() bool { diff --git a/internal/op/room.go b/internal/op/room.go index 6e2da43..443422a 100644 --- a/internal/op/room.go +++ b/internal/op/room.go @@ -182,3 +182,16 @@ func (r *Room) SetStatus(playing bool, seek float64, rate float64, timeDiff floa func (r *Room) SetSeekRate(seek float64, rate float64, timeDiff float64) Status { return r.current.SetSeekRate(seek, rate, timeDiff) } + +func (r *Room) SetRoomStatus(status model.RoomStatus) error { + err := db.SetRoomStatus(r.ID, status) + if err != nil { + return err + } + r.Status = status + switch status { + case model.RoomStatusBanned, model.RoomStatusStopped, model.RoomStatusPending: + return CompareAndCloseRoom(r) + } + return nil +} diff --git a/internal/op/rooms.go b/internal/op/rooms.go index b47b726..2111158 100644 --- a/internal/op/rooms.go +++ b/internal/op/rooms.go @@ -39,7 +39,21 @@ func InitRoom(room *model.Room) (*Room, error) { return i.Value(), nil } +var ( + ErrRoomPending = errors.New("room pending, please wait for admin to approve") + ErrRoomStopped = errors.New("room stopped") + ErrRoomBanned = errors.New("room banned") +) + func LoadOrInitRoom(room *model.Room) (*Room, error) { + switch room.Status { + case model.RoomStatusBanned: + return nil, ErrRoomBanned + case model.RoomStatusPending: + return nil, ErrRoomPending + case model.RoomStatusStopped: + return nil, ErrRoomStopped + } t := time.Duration(settings.RoomTTL.Get()) i, loaded := roomCache.LoadOrStore(room.ID, &Room{ Room: *room, @@ -52,14 +66,6 @@ func LoadOrInitRoom(room *model.Room) (*Room, error) { if loaded { i.SetExpiration(time.Now().Add(t)) } - switch room.Status { - case model.RoomStatusBanned: - return nil, errors.New("room banned") - case model.RoomStatusPending: - return nil, errors.New("room pending, please wait for admin to approve") - case model.RoomStatusStopped: - return nil, errors.New("room stopped") - } return i.Value(), nil } @@ -79,6 +85,19 @@ func CloseRoom(roomID string) error { return nil } +func CompareAndCloseRoom(room *Room) error { + r, loaded := roomCache.Load(room.ID) + if loaded { + if r.Value() != room { + return nil + } + if roomCache.CompareAndDelete(room.ID, r) { + r.Value().close() + } + } + return nil +} + func LoadRoomByID(id string) (*Room, error) { r2, loaded := roomCache.Load(id) if loaded { diff --git a/internal/op/users.go b/internal/op/users.go index 53a222a..337885b 100644 --- a/internal/op/users.go +++ b/internal/op/users.go @@ -13,6 +13,11 @@ import ( var userCache gcache.Cache +var ( + ErrUserBanned = errors.New("user banned") + ErrUserPending = errors.New("user pending, please wait for admin to approve") +) + func GetUserById(id string) (*User, error) { i, err := userCache.Get(id) if err == nil { @@ -24,6 +29,13 @@ func GetUserById(id string) (*User, error) { return nil, err } + switch u.Role { + case model.RoleBanned: + return nil, ErrUserBanned + case model.RolePending: + return nil, ErrUserPending + } + u2 := &User{ User: *u, } diff --git a/server/handlers/admin.go b/server/handlers/admin.go index 6a61701..5e42677 100644 --- a/server/handlers/admin.go +++ b/server/handlers/admin.go @@ -172,18 +172,31 @@ func PendingUsers(ctx *gin.Context) { })) } -func ApprovePendingUser(Authorization, userID string) error { - user, err := op.GetUserById(userID) +func ApprovePendingUser(ctx *gin.Context) { + req := model.UserIDReq{} + if err := model.Decode(ctx, &req); err != nil { + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) + return + } + + user, err := db.GetUserByID(req.ID) if err != nil { - return err + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) + return } + if !user.IsPending() { - return errors.New("user is not pending") + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("user is not pending")) + return } - if err := user.SetRole(dbModel.RoleUser); err != nil { - return err + + err = db.SetRoleByID(req.ID, dbModel.RoleUser) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return } - return nil + + ctx.Status(http.StatusNoContent) } func BanUser(ctx *gin.Context) { @@ -197,7 +210,15 @@ func BanUser(ctx *gin.Context) { u, err := op.GetUserById(req.ID) if err != nil { - ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) + if errors.Is(err, op.ErrUserPending) { + err = db.SetRoleByID(req.ID, dbModel.RoleBanned) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + } else { + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) + } return } @@ -218,3 +239,149 @@ func BanUser(ctx *gin.Context) { ctx.Status(http.StatusNoContent) } + +func PendingRooms(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{ + db.WhereStatus(dbModel.RoomStatusPending), + } + + if keyword := ctx.Query("keyword"); keyword != "" { + scopes = append(scopes, db.WhereRoomNameLike(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("name")) + } else { + scopes = append(scopes, db.OrderByAsc("name")) + } + 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 keyword := ctx.Query("keyword"); keyword != "" { + // search mode, all, name, creator + switch ctx.DefaultQuery("search", "all") { + case "all": + scopes = append(scopes, db.WhereRoomNameLikeOrCreatorIn(keyword, db.GerUsersIDByUsernameLike(keyword))) + case "name": + scopes = append(scopes, db.WhereRoomNameLike(keyword)) + case "creator": + scopes = append(scopes, db.WhereCreatorIDIn(db.GerUsersIDByUsernameLike(keyword))) + } + } + + ctx.JSON(http.StatusOK, model.NewApiDataResp(gin.H{ + "total": db.GetAllRoomsWithoutHiddenCount(scopes...), + "list": genRoomListResp(append(scopes, db.Paginate(page, pageSize))...), + })) +} + +func ApprovePendingRoom(ctx *gin.Context) { + req := model.RoomIDReq{} + if err := model.Decode(ctx, &req); err != nil { + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) + return + } + + room, err := db.GetRoomByID(req.Id) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) + return + } + + if !room.IsPending() { + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("room is not pending")) + return + } + + err = db.SetRoomStatus(req.Id, dbModel.RoomStatusActive) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + + ctx.Status(http.StatusNoContent) +} + +func BanRoom(ctx *gin.Context) { + user := ctx.MustGet("user").(*op.User) + + req := model.RoomIDReq{} + if err := model.Decode(ctx, &req); err != nil { + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) + return + } + + room, err := op.LoadOrInitRoomByID(req.Id) + if err != nil { + if errors.Is(err, op.ErrRoomPending) || errors.Is(err, op.ErrRoomStopped) { + err = db.SetRoomStatus(req.Id, dbModel.RoomStatusBanned) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + } else { + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) + } + return + } + + creator, err := db.GetUserByID(room.CreatorID) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) + return + } + + if creator.ID == user.ID { + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("cannot ban yourself")) + return + } + + if creator.IsAdmin() { + ctx.AbortWithStatusJSON(http.StatusForbidden, model.NewApiErrorStringResp("no permission")) + return + } + + if room.IsBanned() { + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("room is already banned")) + return + } + + err = room.SetRoomStatus(dbModel.RoomStatusBanned) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + + ctx.Status(http.StatusNoContent) +} diff --git a/server/handlers/init.go b/server/handlers/init.go index 53cad46..f188cff 100644 --- a/server/handlers/init.go +++ b/server/handlers/init.go @@ -50,6 +50,18 @@ func Init(e *gin.Engine) { admin.POST("/settings", EditAdminSettings) admin.GET("/users", Users) + + admin.GET("/pending/users", PendingUsers) + + admin.GET("/pending/rooms", PendingRooms) + + admin.POST("/approve/user", ApprovePendingUser) + + admin.POST("/approve/room", ApprovePendingRoom) + + admin.POST("/ban/user", BanUser) + + admin.POST("/ban/room", BanRoom) } { @@ -57,7 +69,7 @@ func Init(e *gin.Engine) { root.POST("/addAdmin", AddAdmin) - root.POST("deleteAdmin", DeleteAdmin) + root.POST("/deleteAdmin", DeleteAdmin) } } diff --git a/server/handlers/room.go b/server/handlers/room.go index c910a65..c234e84 100644 --- a/server/handlers/room.go +++ b/server/handlers/room.go @@ -7,6 +7,7 @@ import ( "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/middlewares" @@ -91,10 +92,9 @@ func RoomList(ctx *gin.Context) { var desc = ctx.DefaultQuery("sort", "desc") == "desc" - // search mode, all, name, creator - var search = ctx.DefaultQuery("search", "all") - - scopes := []func(db *gorm.DB) *gorm.DB{} + scopes := []func(db *gorm.DB) *gorm.DB{ + db.WhereStatus(dbModel.RoomStatusActive), + } switch order { case "createdAt": @@ -103,53 +103,35 @@ func RoomList(ctx *gin.Context) { } else { scopes = append(scopes, db.OrderByCreatedAtAsc) } - if keyword := ctx.Query("keyword"); keyword != "" { - switch search { - case "all": - scopes = append(scopes, db.WhereRoomNameLikeOrCreatorIn(keyword, db.GerUsersIDByUsernameLike(keyword))) - case "name": - scopes = append(scopes, db.WhereRoomNameLike(keyword)) - case "creator": - scopes = append(scopes, db.WhereCreatorIDIn(db.GerUsersIDByUsernameLike(keyword))) - } - } case "roomName": if desc { scopes = append(scopes, db.OrderByDesc("name")) } else { scopes = append(scopes, db.OrderByAsc("name")) } - if keyword := ctx.Query("keyword"); keyword != "" { - switch search { - case "all": - scopes = append(scopes, db.WhereRoomNameLikeOrCreatorIn(keyword, db.GerUsersIDByUsernameLike(keyword))) - case "name": - scopes = append(scopes, db.WhereRoomNameLike(keyword)) - case "creator": - scopes = append(scopes, db.WhereCreatorIDIn(db.GerUsersIDByUsernameLike(keyword))) - } - } case "roomId": if desc { scopes = append(scopes, db.OrderByIDDesc) } else { scopes = append(scopes, db.OrderByIDAsc) } - if keyword := ctx.Query("keyword"); keyword != "" { - switch search { - case "all": - scopes = append(scopes, db.WhereRoomNameLikeOrCreatorIn(keyword, db.GerUsersIDByUsernameLike(keyword))) - case "name": - scopes = append(scopes, db.WhereRoomNameLike(keyword)) - case "creator": - scopes = append(scopes, db.WhereCreatorIDIn(db.GerUsersIDByUsernameLike(keyword))) - } - } default: ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("not support order")) return } + if keyword := ctx.Query("keyword"); keyword != "" { + // search mode, all, name, creator + switch ctx.DefaultQuery("search", "all") { + case "all": + scopes = append(scopes, db.WhereRoomNameLikeOrCreatorIn(keyword, db.GerUsersIDByUsernameLike(keyword))) + case "name": + scopes = append(scopes, db.WhereRoomNameLike(keyword)) + case "creator": + scopes = append(scopes, db.WhereCreatorIDIn(db.GerUsersIDByUsernameLike(keyword))) + } + } + ctx.JSON(http.StatusOK, model.NewApiDataResp(gin.H{ "total": db.GetAllRoomsWithoutHiddenCount(scopes...), "list": genRoomListResp(append(scopes, db.Paginate(page, pageSize))...), diff --git a/server/handlers/root.go b/server/handlers/root.go index 6e41f59..22b0f62 100644 --- a/server/handlers/root.go +++ b/server/handlers/root.go @@ -44,7 +44,7 @@ func AddAdmin(ctx *gin.Context) { ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorStringResp("user not found")) return } - if u.Role >= dbModel.RoleAdmin { + if u.IsAdmin() { ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("user is already admin")) return } @@ -75,7 +75,7 @@ func DeleteAdmin(ctx *gin.Context) { ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorStringResp("user not found")) return } - if u.Role == dbModel.RoleRoot { + if u.IsRoot() { ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("cannot remove root")) return } diff --git a/server/middlewares/auth.go b/server/middlewares/auth.go index aa6106e..fdc6d76 100644 --- a/server/middlewares/auth.go +++ b/server/middlewares/auth.go @@ -76,12 +76,6 @@ func AuthRoom(Authorization string) (*op.User, *op.Room, error) { if err != nil { return nil, nil, err } - 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,12 +102,6 @@ func AuthUser(Authorization string) (*op.User, error) { if err != nil { return nil, err } - 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 } diff --git a/server/model/room.go b/server/model/room.go index 22408cc..e5b6612 100644 --- a/server/model/room.go +++ b/server/model/room.go @@ -107,17 +107,18 @@ func (s *SetRoomPasswordReq) Validate() error { return nil } -type UserIdReq struct { - UserId string `json:"userId"` +type RoomIDReq struct { + Id string `json:"id"` } -func (u *UserIdReq) Decode(ctx *gin.Context) error { - return json.NewDecoder(ctx.Request.Body).Decode(u) +func (r *RoomIDReq) Decode(ctx *gin.Context) error { + return json.NewDecoder(ctx.Request.Body).Decode(r) } -func (u *UserIdReq) Validate() error { - if len(u.UserId) != 36 { - return ErrEmptyUserId +func (r *RoomIDReq) Validate() error { + if len(r.Id) != 36 { + return ErrEmptyRoomName } + return nil }