From 9b0b9ff1f2a284222d0a5de17eccdde98ce27a18 Mon Sep 17 00:00:00 2001 From: zijiren233 Date: Sat, 13 Apr 2024 23:46:20 +0800 Subject: [PATCH] Feat: init permission control --- cmd/admin/show.go | 5 +- internal/bootstrap/db.go | 2 +- internal/db/db.go | 14 +- internal/db/member.go | 133 ++++++++++++++ internal/db/relation.go | 72 -------- internal/db/room.go | 67 ++++--- internal/db/update.go | 3 +- internal/db/user.go | 36 ++-- internal/model/member.go | 173 ++++++++++++++++++ internal/model/relation.go | 66 ------- internal/model/room.go | 56 ++++-- internal/model/user.go | 26 +-- internal/op/client.go | 27 +++ internal/op/current.go | 10 +- internal/op/movie.go | 8 +- internal/op/movies.go | 34 ++-- internal/op/room.go | 171 ++++++++++++++---- internal/op/user.go | 163 +++++++++++++++-- server/handlers/admin.go | 171 ++++++++++++++---- server/handlers/init.go | 37 +++- server/handlers/member.go | 339 +++++++++++++++++++++++++++++++++++ server/handlers/movie.go | 84 ++++----- server/handlers/room.go | 138 +++++++------- server/handlers/user.go | 26 ++- server/handlers/websocket.go | 40 +++-- server/middlewares/auth.go | 46 ++++- server/model/member.go | 48 +++++ server/model/room.go | 19 +- 28 files changed, 1531 insertions(+), 483 deletions(-) create mode 100644 internal/db/member.go delete mode 100644 internal/db/relation.go create mode 100644 internal/model/member.go delete mode 100644 internal/model/relation.go create mode 100644 server/handlers/member.go create mode 100644 server/model/member.go diff --git a/cmd/admin/show.go b/cmd/admin/show.go index ac0e58e..2a21b3a 100644 --- a/cmd/admin/show.go +++ b/cmd/admin/show.go @@ -20,7 +20,10 @@ var ShowCmd = &cobra.Command{ ).Run() }, RunE: func(cmd *cobra.Command, args []string) error { - admins := db.GetAdmins() + admins, err := db.GetAdmins() + if err != nil { + fmt.Printf("get admins failed: %s\n", err.Error()) + } for _, admin := range admins { fmt.Printf("id: %s\tusername: %s\n", admin.ID, admin.Username) } diff --git a/internal/bootstrap/db.go b/internal/bootstrap/db.go index e29975d..4fe9337 100644 --- a/internal/bootstrap/db.go +++ b/internal/bootstrap/db.go @@ -31,7 +31,7 @@ func InitDatabase(ctx context.Context) (err error) { Logger: newDBLogger(), PrepareStmt: true, DisableForeignKeyConstraintWhenMigrating: false, - IgnoreRelationshipsWhenMigrating: true, + IgnoreRelationshipsWhenMigrating: false, }) d, err := gorm.Open(dialector, opts...) if err != nil { diff --git a/internal/db/db.go b/internal/db/db.go index 886b7df..c121c0e 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -109,9 +109,9 @@ func WhereRoomID(roomID string) func(db *gorm.DB) *gorm.DB { } } -func PreloadRoomUserRelations(scopes ...func(*gorm.DB) *gorm.DB) func(db *gorm.DB) *gorm.DB { +func PreloadRoomMembers(scopes ...func(*gorm.DB) *gorm.DB) func(db *gorm.DB) *gorm.DB { return func(db *gorm.DB) *gorm.DB { - return db.Preload("RoomUserRelations", func(db *gorm.DB) *gorm.DB { + return db.Preload("RoomMembers", func(db *gorm.DB) *gorm.DB { return db.Scopes(scopes...) }) } @@ -243,13 +243,7 @@ func WhereIDIn(ids []string) func(db *gorm.DB) *gorm.DB { func WhereRoomSettingWithoutHidden() func(db *gorm.DB) *gorm.DB { return func(db *gorm.DB) *gorm.DB { - return db.Where("settings_hidden = ?", false) - } -} - -func WhereRoomSettingHidden() func(db *gorm.DB) *gorm.DB { - return func(db *gorm.DB) *gorm.DB { - return db.Where("settings_hidden = ?", true) + return db.Where("hidden = ?", false) } } @@ -264,7 +258,7 @@ func WhereIDLike(id string) func(db *gorm.DB) *gorm.DB { } } -func WhereRoomUserStatus(status model.RoomUserStatus) func(db *gorm.DB) *gorm.DB { +func WhereRoomMemberStatus(status model.RoomMemberStatus) func(db *gorm.DB) *gorm.DB { return func(db *gorm.DB) *gorm.DB { return db.Where("status = ?", status) } diff --git a/internal/db/member.go b/internal/db/member.go new file mode 100644 index 0000000..15c0f41 --- /dev/null +++ b/internal/db/member.go @@ -0,0 +1,133 @@ +package db + +import ( + "fmt" + + "github.com/synctv-org/synctv/internal/model" + "gorm.io/gorm" +) + +type CreateRoomMemberRelationConfig func(r *model.RoomMember) + +func WithRoomMemberStatus(status model.RoomMemberStatus) CreateRoomMemberRelationConfig { + return func(r *model.RoomMember) { + r.Status = status + } +} + +func WithRoomMemberRole(role model.RoomMemberRole) CreateRoomMemberRelationConfig { + return func(r *model.RoomMember) { + r.Role = role + } +} + +func WithRoomMemberRelationPermissions(permissions model.RoomMemberPermission) CreateRoomMemberRelationConfig { + return func(r *model.RoomMember) { + r.Permissions = permissions + } +} + +func WithRoomMemberAdminPermissions(permissions model.RoomAdminPermission) CreateRoomMemberRelationConfig { + return func(r *model.RoomMember) { + r.AdminPermissions = permissions + } +} + +func FirstOrCreateRoomMemberRelation(roomID, userID string, conf ...CreateRoomMemberRelationConfig) (*model.RoomMember, error) { + roomMemberRelation := &model.RoomMember{} + d := &model.RoomMember{ + RoomID: roomID, + UserID: userID, + Role: model.RoomMemberRoleMember, + Status: model.RoomMemberStatusPending, + Permissions: model.NoPermission, + AdminPermissions: model.NoAdminPermission, + } + for _, c := range conf { + c(d) + } + err := db.Where("room_id = ? AND user_id = ?", roomID, userID).Attrs(d).FirstOrCreate(roomMemberRelation).Error + return roomMemberRelation, err +} + +func GetRoomMemberRelation(roomID, userID string) (*model.RoomMember, error) { + roomMemberRelation := &model.RoomMember{} + err := db.Where("room_id = ? AND user_id = ?", roomID, userID).First(roomMemberRelation).Error + return roomMemberRelation, HandleNotFound(err, "room or user") +} + +func RoomApprovePendingMember(roomID, userID string) error { + roomMember := &model.RoomMember{} + err := db.Where("room_id = ? AND user_id = ?", roomID, userID).First(roomMember).Error + if err != nil { + return err + } + if roomMember.Status != model.RoomMemberStatusPending { + return fmt.Errorf("user is not pending") + } + err = db.Model(&model.RoomMember{}).Where("room_id = ? AND user_id = ? AND status = ?", roomID, userID, model.RoomMemberStatusPending).Update("status", model.RoomMemberStatusActive).Error + if err != nil && gorm.ErrRecordNotFound != err { + return fmt.Errorf("update status failed") + } + return err +} + +func RoomBanMember(roomID, userID string) error { + err := db.Model(&model.RoomMember{}).Where("room_id = ? AND user_id = ?", roomID, userID).Update("status", model.RoomMemberStatusBanned).Error + return HandleNotFound(err, "room or user") +} + +func RoomUnbanMember(roomID, userID string) error { + err := db.Model(&model.RoomMember{}).Where("room_id = ? AND user_id = ?", roomID, userID).Update("status", model.RoomMemberStatusActive).Error + return HandleNotFound(err, "room or user") +} + +func SetMemberPermissions(roomID string, userID string, permission model.RoomMemberPermission) error { + err := db.Model(&model.RoomMember{}).Where("room_id = ? AND user_id = ?", roomID, userID).Update("permissions", permission).Error + return HandleNotFound(err, "room or user") +} + +func AddMemberPermissions(roomID string, userID string, permission model.RoomMemberPermission) error { + err := db.Model(&model.RoomMember{}).Where("room_id = ? AND user_id = ?", roomID, userID).Update("permissions", db.Raw("permissions | ?", permission)).Error + return HandleNotFound(err, "room or user") +} + +func RemoveMemberPermissions(roomID string, userID string, permission model.RoomMemberPermission) error { + err := db.Model(&model.RoomMember{}).Where("room_id = ? AND user_id = ?", roomID, userID).Update("permissions", db.Raw("permissions & ?", ^permission)).Error + return HandleNotFound(err, "room or user") +} + +// func GetAllRoomMembersRelationCount(roomID string, scopes ...func(*gorm.DB) *gorm.DB) (int64, error) { +// var count int64 +// err := db.Model(&model.RoomMember{}).Where("room_id = ?", roomID).Scopes(scopes...).Count(&count).Error +// return count, err +// } + +func RoomSetAdminPermissions(roomID, userID string, permissions model.RoomAdminPermission) error { + err := db.Model(&model.RoomMember{}).Where("room_id = ? AND user_id = ?", roomID, userID).Update("admin_permissions", permissions).Error + return HandleNotFound(err, "room or user") +} + +func RoomAddAdminPermissions(roomID, userID string, permissions model.RoomAdminPermission) error { + err := db.Model(&model.RoomMember{}).Where("room_id = ? AND user_id = ?", roomID, userID).Update("admin_permissions", db.Raw("admin_permissions | ?", permissions)).Error + return HandleNotFound(err, "room or user") +} + +func RoomRemoveAdminPermissions(roomID, userID string, permissions model.RoomAdminPermission) error { + err := db.Model(&model.RoomMember{}).Where("room_id = ? AND user_id = ?", roomID, userID).Update("admin_permissions", db.Raw("admin_permissions & ?", ^permissions)).Error + return HandleNotFound(err, "room or user") +} + +func RoomSetAdmin(roomID, userID string, permissions model.RoomAdminPermission) error { + return db.Model(&model.RoomMember{}).Where("room_id = ? AND user_id = ?", roomID, userID).Updates(map[string]interface{}{ + "role": model.RoomMemberRoleAdmin, + "admin_permissions": permissions, + }).Error +} + +func RoomSetMember(roomID, userID string, permissions model.RoomMemberPermission) error { + return db.Model(&model.RoomMember{}).Where("room_id = ? AND user_id = ?", roomID, userID).Updates(map[string]interface{}{ + "role": model.RoomMemberRoleMember, + "permissions": permissions, + }).Error +} diff --git a/internal/db/relation.go b/internal/db/relation.go deleted file mode 100644 index d659b96..0000000 --- a/internal/db/relation.go +++ /dev/null @@ -1,72 +0,0 @@ -package db - -import ( - "github.com/synctv-org/synctv/internal/model" - "gorm.io/gorm" -) - -type CreateRoomUserRelationConfig func(r *model.RoomUserRelation) - -func WithRoomUserRelationStatus(status model.RoomUserStatus) CreateRoomUserRelationConfig { - return func(r *model.RoomUserRelation) { - r.Status = status - } -} - -func WithRoomUserRelationPermissions(permissions model.RoomUserPermission) CreateRoomUserRelationConfig { - return func(r *model.RoomUserRelation) { - r.Permissions = permissions - } -} - -func FirstOrCreateRoomUserRelation(roomID, userID string, conf ...CreateRoomUserRelationConfig) (*model.RoomUserRelation, error) { - roomUserRelation := &model.RoomUserRelation{} - d := &model.RoomUserRelation{ - RoomID: roomID, - UserID: userID, - Permissions: model.DefaultPermissions, - } - for _, c := range conf { - c(d) - } - err := db.Where("room_id = ? AND user_id = ?", roomID, userID).Attrs(d).FirstOrCreate(roomUserRelation).Error - return roomUserRelation, err -} - -func GetRoomUserRelation(roomID, userID string) (*model.RoomUserRelation, error) { - roomUserRelation := &model.RoomUserRelation{} - err := db.Where("room_id = ? AND user_id = ?", roomID, userID).First(roomUserRelation).Error - return roomUserRelation, HandleNotFound(err, "room or user") -} - -func SetRoomUserStatus(roomID string, userID string, status model.RoomUserStatus) error { - err := db.Model(&model.RoomUserRelation{}).Where("room_id = ? AND user_id = ?", roomID, userID).Update("status", status).Error - return HandleNotFound(err, "room or user") -} - -func SetUserPermission(roomID string, userID string, permission model.RoomUserPermission) error { - err := db.Model(&model.RoomUserRelation{}).Where("room_id = ? AND user_id = ?", roomID, userID).Update("permissions", permission).Error - return HandleNotFound(err, "room or user") -} - -func AddUserPermission(roomID string, userID string, permission model.RoomUserPermission) error { - err := db.Model(&model.RoomUserRelation{}).Where("room_id = ? AND user_id = ?", roomID, userID).Update("permissions", db.Raw("permissions | ?", permission)).Error - return HandleNotFound(err, "room or user") -} - -func RemoveUserPermission(roomID string, userID string, permission model.RoomUserPermission) error { - err := db.Model(&model.RoomUserRelation{}).Where("room_id = ? AND user_id = ?", roomID, userID).Update("permissions", db.Raw("permissions & ?", ^permission)).Error - return HandleNotFound(err, "room or user") -} - -func GetAllRoomUsersRelation(roomID string, scopes ...func(*gorm.DB) *gorm.DB) []*model.RoomUserRelation { - var roomUserRelations []*model.RoomUserRelation - db.Where("room_id = ?", roomID).Scopes(scopes...).Find(&roomUserRelations) - return roomUserRelations -} - -func GetAllRoomUsersRelationCount(roomID string, scopes ...func(*gorm.DB) *gorm.DB) int64 { - var count int64 - db.Model(&model.RoomUserRelation{}).Where("room_id = ?", roomID).Scopes(scopes...).Count(&count) - return count -} diff --git a/internal/db/room.go b/internal/db/room.go index e481a6a..b6755b9 100644 --- a/internal/db/room.go +++ b/internal/db/room.go @@ -12,7 +12,7 @@ import ( type CreateRoomConfig func(r *model.Room) -func WithSetting(setting model.RoomSettings) CreateRoomConfig { +func WithSetting(setting *model.RoomSettings) CreateRoomConfig { return func(r *model.Room) { r.Settings = setting } @@ -21,17 +21,19 @@ func WithSetting(setting model.RoomSettings) CreateRoomConfig { func WithCreator(creator *model.User) CreateRoomConfig { return func(r *model.Room) { r.CreatorID = creator.ID - r.GroupUserRelations = []*model.RoomUserRelation{ + r.GroupUserRelations = []*model.RoomMember{ { - UserID: creator.ID, - Status: model.RoomUserStatusActive, - Permissions: model.PermissionAll, + UserID: creator.ID, + Status: model.RoomMemberStatusActive, + Role: model.RoomMemberRoleCreator, + Permissions: model.AllPermissions, + AdminPermissions: model.AllAdminPermissions, }, } } } -func WithRelations(relations []*model.RoomUserRelation) CreateRoomConfig { +func WithRelations(relations []*model.RoomMember) CreateRoomConfig { return func(r *model.Room) { r.GroupUserRelations = append(r.GroupUserRelations, relations...) } @@ -43,10 +45,17 @@ func WithStatus(status model.RoomStatus) CreateRoomConfig { } } +func WithSettingHidden(hidden bool) CreateRoomConfig { + return func(r *model.Room) { + r.Settings.Hidden = hidden + } +} + // if maxCount is 0, it will be ignored func CreateRoom(name, password string, maxCount int64, conf ...CreateRoomConfig) (*model.Room, error) { r := &model.Room{ - Name: name, + Name: name, + Settings: model.DefaultRoomSettings(), } for _, c := range conf { c(r) @@ -83,15 +92,29 @@ func GetRoomByID(id string) (*model.Room, error) { return nil, errors.New("room id is not 32 bit") } r := &model.Room{} - err := db.Where("id = ?", id).First(r).Error + err := db. + Where("id = ?", id). + Preload("Settings", "id = ?", id). + First(r).Error return r, HandleNotFound(err, "room") } -func SaveRoomSettings(roomID string, setting model.RoomSettings) error { - err := db.Model(&model.Room{}).Where("id = ?", roomID).Update("setting", setting).Error +func SaveRoomSettings(roomID string, settings *model.RoomSettings) error { + settings.ID = roomID + err := db.Save(settings).Error return HandleNotFound(err, "room") } +func UpdateRoomSettings(roomID string, settings map[string]interface{}) (*model.RoomSettings, error) { + rs := &model.RoomSettings{ + ID: roomID, + } + err := db.Model(rs). + Clauses(clause.Returning{}). + Updates(settings).Error + return rs, HandleNotFound(err, "room") +} + func DeleteRoomByID(roomID string) error { err := db.Unscoped().Select(clause.Associations).Delete(&model.Room{ID: roomID}).Error return HandleNotFound(err, "room") @@ -114,28 +137,28 @@ func SetRoomHashedPassword(roomID string, hashedPassword []byte) error { return HandleNotFound(err, "room") } -func GetAllRooms(scopes ...func(*gorm.DB) *gorm.DB) []*model.Room { +func GetAllRooms(scopes ...func(*gorm.DB) *gorm.DB) ([]*model.Room, error) { rooms := []*model.Room{} - db.Scopes(scopes...).Find(&rooms) - return rooms + err := db.Scopes(scopes...).Find(&rooms).Error + return rooms, err } -func GetAllRoomsCount(scopes ...func(*gorm.DB) *gorm.DB) int64 { +func GetAllRoomsCount(scopes ...func(*gorm.DB) *gorm.DB) (int64, error) { var count int64 - db.Model(&model.Room{}).Scopes(scopes...).Count(&count) - return count + err := db.Model(&model.Room{}).Scopes(scopes...).Count(&count).Error + return count, err } -func GetAllRoomsAndCreator(scopes ...func(*gorm.DB) *gorm.DB) []*model.Room { +func GetAllRoomsAndCreator(scopes ...func(*gorm.DB) *gorm.DB) ([]*model.Room, error) { rooms := []*model.Room{} - db.Preload("Creator").Scopes(scopes...).Find(&rooms) - return rooms + err := db.Preload("Creator").Scopes(scopes...).Find(&rooms).Error + return rooms, err } -func GetAllRoomsByUserID(userID string) []*model.Room { +func GetAllRoomsByUserID(userID string) ([]*model.Room, error) { rooms := []*model.Room{} - db.Where("creator_id = ?", userID).Find(&rooms) - return rooms + err := db.Where("creator_id = ?", userID).Find(&rooms).Error + return rooms, err } func SetRoomStatus(roomID string, status model.RoomStatus) error { diff --git a/internal/db/update.go b/internal/db/update.go index 3a9e89f..7ddd059 100644 --- a/internal/db/update.go +++ b/internal/db/update.go @@ -22,7 +22,8 @@ var models = []any{ new(model.User), new(model.UserProvider), new(model.Room), - new(model.RoomUserRelation), + new(model.RoomSettings), + new(model.RoomMember), new(model.Movie), new(model.BilibiliVendor), new(model.AlistVendor), diff --git a/internal/db/user.go b/internal/db/user.go index 783b91c..8c4b58c 100644 --- a/internal/db/user.go +++ b/internal/db/user.go @@ -236,22 +236,22 @@ func GetUserByUsername(username string) (*model.User, error) { return u, HandleNotFound(err, "user") } -func GetUserByUsernameLike(username string, scopes ...func(*gorm.DB) *gorm.DB) []*model.User { +func GetUserByUsernameLike(username string, scopes ...func(*gorm.DB) *gorm.DB) ([]*model.User, error) { var users []*model.User - db.Where(`username LIKE ?`, fmt.Sprintf("%%%s%%", username)).Scopes(scopes...).Find(&users) - return users + err := db.Where(`username LIKE ?`, fmt.Sprintf("%%%s%%", username)).Scopes(scopes...).Find(&users).Error + return users, err } -func GerUsersIDByUsernameLike(username string, scopes ...func(*gorm.DB) *gorm.DB) []string { +func GerUsersIDByUsernameLike(username string, scopes ...func(*gorm.DB) *gorm.DB) ([]string, error) { var ids []string - db.Model(&model.User{}).Where(`username LIKE ?`, fmt.Sprintf("%%%s%%", username)).Scopes(scopes...).Pluck("id", &ids) - return ids + err := db.Model(&model.User{}).Where(`username LIKE ?`, fmt.Sprintf("%%%s%%", username)).Scopes(scopes...).Pluck("id", &ids).Error + return ids, err } -func GerUsersIDByIDLike(id string, scopes ...func(*gorm.DB) *gorm.DB) []string { +func GerUsersIDByIDLike(id string, scopes ...func(*gorm.DB) *gorm.DB) ([]string, error) { var ids []string - db.Model(&model.User{}).Where(`id LIKE ?`, utils.LIKE(id)).Scopes(scopes...).Pluck("id", &ids) - return ids + err := db.Model(&model.User{}).Where(`id LIKE ?`, utils.LIKE(id)).Scopes(scopes...).Pluck("id", &ids).Error + return ids, err } func GetUserByIDOrUsernameLike(idOrUsername string, scopes ...func(*gorm.DB) *gorm.DB) ([]*model.User, error) { @@ -332,10 +332,10 @@ func RemoveAdmin(u *model.User) error { return SaveUser(u) } -func GetAdmins() []*model.User { +func GetAdmins() ([]*model.User, error) { var users []*model.User - db.Where("role == ?", model.RoleAdmin).Find(&users) - return users + err := db.Where("role == ?", model.RoleAdmin).Find(&users).Error + return users, err } func AddAdminByID(userID string) error { @@ -395,16 +395,16 @@ func SetUsernameByID(userID string, username string) error { return HandleNotFound(err, "user") } -func GetAllUserCount(scopes ...func(*gorm.DB) *gorm.DB) int64 { +func GetAllUserCount(scopes ...func(*gorm.DB) *gorm.DB) (int64, error) { var count int64 - db.Model(&model.User{}).Scopes(scopes...).Count(&count) - return count + err := db.Model(&model.User{}).Scopes(scopes...).Count(&count).Error + return count, err } -func GetAllUsers(scopes ...func(*gorm.DB) *gorm.DB) []*model.User { +func GetAllUsers(scopes ...func(*gorm.DB) *gorm.DB) ([]*model.User, error) { var users []*model.User - db.Scopes(scopes...).Find(&users) - return users + err := db.Scopes(scopes...).Find(&users).Error + return users, err } func SetUserHashedPassword(id string, hashedPassword []byte) error { diff --git a/internal/model/member.go b/internal/model/member.go new file mode 100644 index 0000000..b3f2e49 --- /dev/null +++ b/internal/model/member.go @@ -0,0 +1,173 @@ +package model + +import ( + "errors" + "math" + "time" +) + +type RoomMemberStatus uint64 + +const ( + RoomMemberStatusUnknown RoomMemberStatus = iota + RoomMemberStatusBanned + RoomMemberStatusPending + RoomMemberStatusActive +) + +func (r RoomMemberStatus) String() string { + switch r { + case RoomMemberStatusBanned: + return "banned" + case RoomMemberStatusPending: + return "pending" + case RoomMemberStatusActive: + return "active" + default: + return "unknown" + } +} + +func (r RoomMemberStatus) IsPending() bool { + return r == RoomMemberStatusPending +} + +func (r RoomMemberStatus) IsActive() bool { + return r == RoomMemberStatusActive +} + +func (r RoomMemberStatus) IsBanned() bool { + return r == RoomMemberStatusBanned +} + +type RoomMemberPermission uint32 + +const ( + PermissionGetMovieList RoomMemberPermission = 2 << iota + PermissionAddMovie + PermissionDeleteMovie + PermissionEditMovie + PermissionSetCurrentMovie + PermissionSetCurrentStatus + PermissionSendChatMessage + + AllPermissions RoomMemberPermission = math.MaxUint32 + NoPermission RoomMemberPermission = 0 + DefaultPermissions RoomMemberPermission = PermissionGetMovieList | PermissionSendChatMessage +) + +func (p RoomMemberPermission) RemoveAdmin() RoomMemberPermission { + return p & DefaultPermissions +} + +func (p RoomMemberPermission) Has(permission RoomMemberPermission) bool { + return p&permission == permission +} + +func (p RoomMemberPermission) Add(permission RoomMemberPermission) RoomMemberPermission { + return p | permission +} + +func (p RoomMemberPermission) Remove(permission RoomMemberPermission) RoomMemberPermission { + return p &^ permission +} + +type RoomMemberRole uint + +const ( + RoomMemberRoleUnknown RoomMemberRole = iota + RoomMemberRoleMember + RoomMemberRoleAdmin + RoomMemberRoleCreator +) + +func (r RoomMemberRole) String() string { + switch r { + case RoomMemberRoleMember: + return "member" + case RoomMemberRoleAdmin: + return "admin" + default: + return "unknown" + } +} + +func (r RoomMemberRole) IsCreator() bool { + return r == RoomMemberRoleCreator +} + +func (r RoomMemberRole) IsAdmin() bool { + return r == RoomMemberRoleAdmin || r.IsCreator() +} + +func (r RoomMemberRole) IsMember() bool { + return r == RoomMemberRoleMember || r.IsAdmin() +} + +type RoomAdminPermission uint32 + +const ( + PermissionApprovePendingMember RoomAdminPermission = 1 << iota + PermissionBanRoomMember + PermissionSetUserPermission + PermissionSetRoomSettings + PermissionSetRoomPassword + PermissionDeleteRoom + + AllAdminPermissions RoomAdminPermission = math.MaxUint32 + NoAdminPermission RoomAdminPermission = 0 + DefaultAdminPermissions RoomAdminPermission = PermissionApprovePendingMember | + PermissionBanRoomMember | + PermissionSetUserPermission | + PermissionSetRoomSettings | + PermissionSetRoomPassword +) + +func (p RoomAdminPermission) Has(permission RoomAdminPermission) bool { + return p&permission == permission +} + +func (p RoomAdminPermission) Add(permission RoomAdminPermission) RoomAdminPermission { + return p | permission +} + +func (p RoomAdminPermission) Remove(permission RoomAdminPermission) RoomAdminPermission { + return p &^ permission +} + +type RoomMember struct { + CreatedAt time.Time + UpdatedAt time.Time + UserID string `gorm:"primarykey;type:char(32)"` + RoomID string `gorm:"primarykey;type:char(32)"` + Status RoomMemberStatus `gorm:"not null;default:2"` + Role RoomMemberRole `gorm:"not null;default:1"` + Permissions RoomMemberPermission + AdminPermissions RoomAdminPermission +} + +var ErrNoPermission = errors.New("no permission") + +func (r *RoomMember) HasPermission(permission RoomMemberPermission) bool { + switch r.Status { + case RoomMemberStatusActive: + return r.Permissions.Has(permission) + default: + return false + } +} + +func (r *RoomMember) HasAdminPermission(permission RoomAdminPermission) bool { + switch r.Status { + case RoomMemberStatusActive: + if !r.Role.IsAdmin() { + return false + } + if r.Role.IsCreator() { + return true + } + return r.AdminPermissions.Has(permission) + default: + return false + } +} diff --git a/internal/model/relation.go b/internal/model/relation.go deleted file mode 100644 index 2763935..0000000 --- a/internal/model/relation.go +++ /dev/null @@ -1,66 +0,0 @@ -package model - -import ( - "errors" - "time" -) - -type RoomUserStatus uint64 - -const ( - RoomUserStatusBanned RoomUserStatus = iota + 1 - RoomUserStatusPending - RoomUserStatusActive -) - -func (r RoomUserStatus) String() string { - switch r { - case RoomUserStatusBanned: - return "banned" - case RoomUserStatusPending: - return "pending" - case RoomUserStatusActive: - return "active" - default: - return "unknown" - } -} - -type RoomUserPermission uint64 - -const ( - PermissionAll RoomUserPermission = 0xffffffff - PermissionEditRoom RoomUserPermission = 1 << iota - PermissionEditUser - PermissionCreateMovie - PermissionEditCurrent - PermissionSendChat -) - -const ( - DefaultPermissions = PermissionCreateMovie | PermissionEditCurrent | PermissionSendChat -) - -func (p RoomUserPermission) Has(permission RoomUserPermission) bool { - return p&permission == permission -} - -type RoomUserRelation struct { - CreatedAt time.Time - UpdatedAt time.Time - UserID string `gorm:"primarykey;type:char(32)"` - RoomID string `gorm:"primarykey;type:char(32)"` - Status RoomUserStatus `gorm:"not null;default:2"` - Permissions RoomUserPermission -} - -var ErrNoPermission = errors.New("no permission") - -func (r *RoomUserRelation) HasPermission(permission RoomUserPermission) bool { - switch r.Status { - case RoomUserStatusActive: - return r.Permissions.Has(permission) - default: - return false - } -} diff --git a/internal/model/room.go b/internal/model/room.go index 7fca671..86cd18b 100644 --- a/internal/model/room.go +++ b/internal/model/room.go @@ -34,13 +34,13 @@ type Room struct { ID string `gorm:"primaryKey;type:char(32)" json:"id"` CreatedAt time.Time UpdatedAt time.Time - Status RoomStatus `gorm:"not null;default:2"` - Name string `gorm:"not null;uniqueIndex;type:varchar(32)"` - Settings RoomSettings `gorm:"embedded;embeddedPrefix:settings_"` - CreatorID string `gorm:"index;type:char(32)"` + Status RoomStatus `gorm:"not null;default:2"` + Name string `gorm:"not null;uniqueIndex;type:varchar(32)"` + Settings *RoomSettings `gorm:"foreignKey:ID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE" json:"settings"` + CreatorID string `gorm:"index;type:char(32)"` HashedPassword []byte - GroupUserRelations []*RoomUserRelation `gorm:"foreignKey:RoomID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` - Movies []*Movie `gorm:"foreignKey:RoomID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` + GroupUserRelations []*RoomMember `gorm:"foreignKey:RoomID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` + Movies []*Movie `gorm:"foreignKey:RoomID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` } func (r *Room) BeforeCreate(tx *gorm.DB) error { @@ -50,16 +50,6 @@ func (r *Room) BeforeCreate(tx *gorm.DB) error { return nil } -type RoomSettings struct { - Hidden bool `json:"hidden"` - CanCreateMovie bool `gorm:"default:true" json:"canCreateMovie"` - CanEditCurrent bool `gorm:"default:true" json:"canEditCurrent"` - CanSendChat bool `gorm:"default:true" json:"canSendChat"` - DisableJoinNewUser bool `gorm:"default:false" json:"disableJoinNewUser"` - JoinNeedReview bool `gorm:"default:false" json:"joinNeedReview"` - UserDefaultPermissions RoomUserPermission `json:"userDefaultPermissions"` -} - func (r *Room) NeedPassword() bool { return len(r.HashedPassword) != 0 } @@ -79,3 +69,37 @@ func (r *Room) IsPending() bool { func (r *Room) IsActive() bool { return r.Status == RoomStatusActive } + +type RoomSettings struct { + ID string `gorm:"primaryKey;type:char(32)" json:"-"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"-"` + Hidden bool `gorm:"default:false" json:"hidden"` + DisableJoinNewUser bool `gorm:"default:false" json:"disable_join_new_user"` + JoinNeedReview bool `gorm:"default:false" json:"join_need_review"` + UserDefaultPermissions RoomMemberPermission `json:"user_default_permissions"` + + CanGetMovieList bool `gorm:"default:true" json:"can_get_movie_list"` + CanAddMovie bool `gorm:"default:true" json:"can_add_movie"` + CanDeleteMovie bool `gorm:"default:true" json:"can_delete_movie"` + CanEditMovie bool `gorm:"default:true" json:"can_edit_movie"` + CanSetCurrentMovie bool `gorm:"default:true" json:"can_set_current_movie"` + CanSetCurrentStatus bool `gorm:"default:true" json:"can_set_current_status"` + CanSendChatMessage bool `gorm:"default:true" json:"can_send_chat_message"` +} + +func DefaultRoomSettings() *RoomSettings { + return &RoomSettings{ + Hidden: false, + DisableJoinNewUser: false, + JoinNeedReview: false, + UserDefaultPermissions: DefaultPermissions, + + CanGetMovieList: true, + CanAddMovie: true, + CanDeleteMovie: true, + CanEditMovie: true, + CanSetCurrentMovie: true, + CanSetCurrentStatus: true, + CanSendChatMessage: true, + } +} diff --git a/internal/model/user.go b/internal/model/user.go index b062547..ad29065 100644 --- a/internal/model/user.go +++ b/internal/model/user.go @@ -42,19 +42,19 @@ type User struct { ID string `gorm:"primaryKey;type:char(32)" json:"id"` CreatedAt time.Time UpdatedAt time.Time - RegisteredByProvider bool `gorm:"not null;default:false"` - RegisteredByEmail bool `gorm:"not null;default:false"` - UserProviders []*UserProvider `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` - Username string `gorm:"not null;uniqueIndex;type:varchar(32)"` - HashedPassword []byte `gorm:"not null"` - Email string `gorm:"type:varchar(128);uniqueIndex:,where:email <> ''"` - Role Role `gorm:"not null;default:2"` - RoomUserRelations []*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"` - BilibiliVendor *BilibiliVendor `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` - AlistVendor []*AlistVendor `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` - EmbyVendor []*EmbyVendor `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` + RegisteredByProvider bool `gorm:"not null;default:false"` + RegisteredByEmail bool `gorm:"not null;default:false"` + UserProviders []*UserProvider `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` + Username string `gorm:"not null;uniqueIndex;type:varchar(32)"` + HashedPassword []byte `gorm:"not null"` + Email string `gorm:"type:varchar(128);uniqueIndex:,where:email <> ''"` + Role Role `gorm:"not null;default:2"` + RoomMembers []*RoomMember `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"` + BilibiliVendor *BilibiliVendor `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` + AlistVendor []*AlistVendor `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` + EmbyVendor []*EmbyVendor `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` } func (u *User) CheckPassword(password string) bool { diff --git a/internal/op/client.go b/internal/op/client.go index 130d328..f2c22b5 100644 --- a/internal/op/client.go +++ b/internal/op/client.go @@ -7,6 +7,8 @@ import ( "time" "github.com/gorilla/websocket" + "github.com/synctv-org/synctv/internal/model" + pb "github.com/synctv-org/synctv/proto/message" ) type Client struct { @@ -41,6 +43,23 @@ func (c *Client) Broadcast(msg Message, conf ...BroadcastConf) error { return c.r.hub.Broadcast(msg, conf...) } +func (c *Client) SendChatMessage(message string) error { + if c.u.HasRoomPermission(c.r, model.PermissionSendChatMessage) { + return model.ErrNoPermission + } + return c.Broadcast(&pb.ElementMessage{ + Type: pb.ElementMessageType_CHAT_MESSAGE, + Time: time.Now().UnixMilli(), + ChatResp: &pb.ChatResp{ + Message: message, + Sender: &pb.Sender{ + Userid: c.u.ID, + Username: c.u.Username, + }, + }, + }) +} + func (c *Client) Send(msg Message) error { c.wg.Add(1) defer c.wg.Done() @@ -75,3 +94,11 @@ func (c *Client) NextWriter(messageType int) (io.WriteCloser, error) { func (c *Client) NextReader() (int, io.Reader, error) { return c.conn.NextReader() } + +func (c *Client) SetSeekRate(seek float64, rate float64, timeDiff float64) (*Status, error) { + return c.u.SetRoomCurrentSeekRate(c.r, seek, rate, timeDiff) +} + +func (c *Client) SetStatus(playing bool, seek float64, rate float64, timeDiff float64) (*Status, error) { + return c.u.SetRoomCurrentStatus(c.r, playing, seek, rate, timeDiff) +} diff --git a/internal/op/current.go b/internal/op/current.go index 46f56d4..01523fc 100644 --- a/internal/op/current.go +++ b/internal/op/current.go @@ -63,18 +63,20 @@ func (c *current) Status() Status { return c.current.Status } -func (c *current) SetStatus(playing bool, seek, rate, timeDiff float64) Status { +func (c *current) SetStatus(playing bool, seek, rate, timeDiff float64) *Status { c.lock.Lock() defer c.lock.Unlock() - return c.current.SetStatus(playing, seek, rate, timeDiff) + s := c.current.SetStatus(playing, seek, rate, timeDiff) + return &s } -func (c *current) SetSeekRate(seek, rate, timeDiff float64) Status { +func (c *current) SetSeekRate(seek, rate, timeDiff float64) *Status { c.lock.Lock() defer c.lock.Unlock() - return c.current.SetSeekRate(seek, rate, timeDiff) + s := c.current.SetSeekRate(seek, rate, timeDiff) + return &s } func (c *Current) UpdateStatus() Status { diff --git a/internal/op/movie.go b/internal/op/movie.go index 9d66bd3..a8a433d 100644 --- a/internal/op/movie.go +++ b/internal/op/movie.go @@ -23,7 +23,7 @@ import ( ) type Movie struct { - model.Movie + *model.Movie channel atomic.Pointer[rtmps.Channel] alistCache atomic.Pointer[cache.AlistMovieCache] bilibiliCache atomic.Pointer[cache.BilibiliMovieCache] @@ -70,7 +70,7 @@ func (m *Movie) ClearCache() { func (m *Movie) AlistCache() *cache.AlistMovieCache { c := m.alistCache.Load() if c == nil { - c = cache.NewAlistMovieCache(&m.Movie) + c = cache.NewAlistMovieCache(m.Movie) if !m.alistCache.CompareAndSwap(nil, c) { return m.AlistCache() } @@ -81,7 +81,7 @@ func (m *Movie) AlistCache() *cache.AlistMovieCache { func (m *Movie) BilibiliCache() *cache.BilibiliMovieCache { c := m.bilibiliCache.Load() if c == nil { - c = cache.NewBilibiliMovieCache(&m.Movie) + c = cache.NewBilibiliMovieCache(m.Movie) if !m.bilibiliCache.CompareAndSwap(nil, c) { return m.BilibiliCache() } @@ -92,7 +92,7 @@ func (m *Movie) BilibiliCache() *cache.BilibiliMovieCache { func (m *Movie) EmbyCache() *cache.EmbyMovieCache { c := m.embyCache.Load() if c == nil { - c = cache.NewEmbyMovieCache(&m.Movie) + c = cache.NewEmbyMovieCache(m.Movie) if !m.embyCache.CompareAndSwap(nil, c) { return m.EmbyCache() } diff --git a/internal/op/movies.go b/internal/op/movies.go index bdca43e..a721380 100644 --- a/internal/op/movies.go +++ b/internal/op/movies.go @@ -23,7 +23,7 @@ func (m *movies) init() { m.once.Do(func() { for _, m2 := range db.GetAllMoviesByRoomID(m.roomID) { m.list.PushBack(&Movie{ - Movie: *m2, + Movie: m2, }) } }) @@ -42,7 +42,7 @@ func (m *movies) AddMovie(mo *model.Movie) error { defer m.lock.Unlock() mo.Position = uint(time.Now().UnixMilli()) movie := &Movie{ - Movie: *mo, + Movie: mo, } err := movie.Validate() @@ -55,7 +55,6 @@ func (m *movies) AddMovie(mo *model.Movie) error { return err } - movie.Movie.ID = mo.ID m.list.PushBack(movie) return nil } @@ -68,7 +67,7 @@ func (m *movies) AddMovies(mos []*model.Movie) error { for _, mo := range mos { mo.Position = uint(time.Now().UnixMilli()) movie := &Movie{ - Movie: *mo, + Movie: mo, } err := movie.Validate() @@ -84,8 +83,7 @@ func (m *movies) AddMovies(mos []*model.Movie) error { return err } - for i, mo := range inited { - mo.Movie.ID = mos[i].ID + for _, mo := range inited { m.list.PushBack(mo) } @@ -113,7 +111,7 @@ func (m *movies) Update(movieId string, movie *model.BaseMovie) error { if err != nil { return err } - return db.SaveMovie(&e.Value.Movie) + return db.SaveMovie(e.Value.Movie) } } return nil @@ -238,21 +236,35 @@ func (m *movies) SwapMoviePositions(id1, id2 string) error { return nil } -func (m *movies) GetMoviesWithPage(page, pageSize int) []*Movie { +func (m *movies) GetMoviesWithPage(page, pageSize int, creator string) ([]*Movie, int) { m.init() m.lock.RLock() defer m.lock.RUnlock() - start, end := utils.GetPageItemsRange(m.list.Len(), page, pageSize) + var total int + if creator != "" { + for e := m.list.Front(); e != nil; e = e.Next() { + if e.Value.Movie.CreatorID == creator { + total++ + } + } + } else { + total = m.list.Len() + } + + start, end := utils.GetPageItemsRange(total, page, pageSize) ms := make([]*Movie, 0, end-start) i := 0 for e := m.list.Front(); e != nil; e = e.Next() { + if creator != "" && e.Value.Movie.CreatorID != creator { + continue + } if i >= start && i < end { ms = append(ms, e.Value) } else if i >= end { - return ms + return ms, total } i++ } - return ms + return ms, total } diff --git a/internal/op/room.go b/internal/op/room.go index 5cfe573..324d9b4 100644 --- a/internal/op/room.go +++ b/internal/op/room.go @@ -6,6 +6,7 @@ import ( "sync/atomic" "github.com/gorilla/websocket" + log "github.com/sirupsen/logrus" "github.com/synctv-org/synctv/internal/db" "github.com/synctv-org/synctv/internal/model" "github.com/synctv-org/synctv/utils" @@ -88,12 +89,55 @@ func (r *Room) AddMovies(movies []*model.Movie) error { return r.movies.AddMovies(movies) } -func (r *Room) HasPermission(userID string, permission model.RoomUserPermission) bool { +func (r *Room) UserRole(userID string) (model.RoomMemberRole, error) { + if r.CreatorID == userID { + return model.RoomMemberRoleCreator, nil + } + rur, err := r.LoadOrCreateRoomMember(userID) + if err != nil { + return model.RoomMemberRoleUnknown, err + } + return rur.Role, nil +} + +// do not use this value for permission determination +func (r *Room) IsAdmin(userID string) bool { + role, err := r.UserRole(userID) + if err != nil { + log.Errorf("get user role failed: %s", err.Error()) + return false + } + return role == model.RoomMemberRoleCreator +} + +func (r *Room) HasAdminPermission(userID string, permission model.RoomAdminPermission) bool { + if r.CreatorID == userID { + return true + } + rur, err := r.LoadOrCreateRoomMember(userID) + if err != nil { + return false + } + return rur.HasAdminPermission(permission) +} + +func (r *Room) UserStatus(userID string) (model.RoomMemberStatus, error) { + if r.CreatorID == userID { + return model.RoomMemberStatusActive, nil + } + rur, err := r.LoadOrCreateRoomMember(userID) + if err != nil { + return model.RoomMemberStatusUnknown, err + } + return rur.Status, nil +} + +func (r *Room) HasPermission(userID string, permission model.RoomMemberPermission) bool { if r.CreatorID == userID { return true } - rur, err := r.LoadOrCreateRoomUserRelation(userID) + rur, err := r.LoadOrCreateRoomMember(userID) if err != nil { return false } @@ -101,24 +145,35 @@ func (r *Room) HasPermission(userID string, permission model.RoomUserPermission) return rur.HasPermission(permission) } -func (r *Room) LoadOrCreateRoomUserRelation(userID string) (*model.RoomUserRelation, error) { - var conf []db.CreateRoomUserRelationConfig - if r.Settings.JoinNeedReview { - conf = []db.CreateRoomUserRelationConfig{db.WithRoomUserRelationStatus(model.RoomUserStatusPending)} +func (r *Room) LoadOrCreateRoomMember(userID string) (*model.RoomMember, error) { + var conf []db.CreateRoomMemberRelationConfig + if r.CreatorID == userID { + conf = append( + conf, + db.WithRoomMemberStatus(model.RoomMemberStatusActive), + db.WithRoomMemberRelationPermissions(model.AllPermissions), + db.WithRoomMemberRole(model.RoomMemberRoleCreator), + db.WithRoomMemberAdminPermissions(model.AllAdminPermissions), + ) } else { - conf = []db.CreateRoomUserRelationConfig{db.WithRoomUserRelationStatus(model.RoomUserStatusActive)} - } - if r.Settings.UserDefaultPermissions != 0 { - conf = append(conf, db.WithRoomUserRelationPermissions(r.Settings.UserDefaultPermissions)) + conf = append( + conf, + db.WithRoomMemberRelationPermissions(r.Settings.UserDefaultPermissions), + ) + if r.Settings.JoinNeedReview { + conf = append(conf, db.WithRoomMemberStatus(model.RoomMemberStatusPending)) + } else { + conf = append(conf, db.WithRoomMemberStatus(model.RoomMemberStatusActive)) + } } - return db.FirstOrCreateRoomUserRelation(r.ID, userID, conf...) + return db.FirstOrCreateRoomMemberRelation(r.ID, userID, conf...) } -func (r *Room) GetRoomUserRelation(userID string) (model.RoomUserPermission, error) { +func (r *Room) GetRoomMemberPermission(userID string) (model.RoomMemberPermission, error) { if r.CreatorID == userID { - return model.PermissionAll, nil + return model.AllPermissions, nil } - ur, err := db.GetRoomUserRelation(r.ID, userID) + ur, err := db.GetRoomMemberRelation(r.ID, userID) if err != nil { return 0, err } @@ -146,20 +201,16 @@ func (r *Room) SetPassword(password string) error { return db.SetRoomHashedPassword(r.ID, hashedPassword) } -func (r *Room) SetUserStatus(userID string, status model.RoomUserStatus) error { - return db.SetRoomUserStatus(r.ID, userID, status) -} - -func (r *Room) SetUserPermission(userID string, permission model.RoomUserPermission) error { - return db.SetUserPermission(r.ID, userID, permission) +func (r *Room) SetUserPermission(userID string, permission model.RoomMemberPermission) error { + return db.SetMemberPermissions(r.ID, userID, permission) } -func (r *Room) AddUserPermission(userID string, permission model.RoomUserPermission) error { - return db.AddUserPermission(r.ID, userID, permission) +func (r *Room) AddUserPermission(userID string, permission model.RoomMemberPermission) error { + return db.AddMemberPermissions(r.ID, userID, permission) } -func (r *Room) RemoveUserPermission(userID string, permission model.RoomUserPermission) error { - return db.RemoveUserPermission(r.ID, userID, permission) +func (r *Room) RemoveUserPermission(userID string, permission model.RoomMemberPermission) error { + return db.RemoveMemberPermissions(r.ID, userID, permission) } func (r *Room) GetMoviesCount() int { @@ -234,8 +285,8 @@ func (r *Room) SwapMoviePositions(id1, id2 string) error { return r.movies.SwapMoviePositions(id1, id2) } -func (r *Room) GetMoviesWithPage(page, pageSize int) []*Movie { - return r.movies.GetMoviesWithPage(page, pageSize) +func (r *Room) GetMoviesWithPage(page, pageSize int, creator string) ([]*Movie, int) { + return r.movies.GetMoviesWithPage(page, pageSize, creator) } func (r *Room) NewClient(user *User, conn *websocket.Conn) (*Client, error) { @@ -258,28 +309,80 @@ func (r *Room) UnregisterClient(cli *Client) error { return r.hub.UnRegClient(cli) } -func (r *Room) SetStatus(playing bool, seek float64, rate float64, timeDiff float64) Status { +func (r *Room) SetCurrentStatus(playing bool, seek float64, rate float64, timeDiff float64) *Status { return r.current.SetStatus(playing, seek, rate, timeDiff) } -func (r *Room) SetSeekRate(seek float64, rate float64, timeDiff float64) Status { +func (r *Room) SetCurrentSeekRate(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) +func (r *Room) SetSettings(settings *model.RoomSettings) error { + err := db.SaveRoomSettings(r.ID, settings) if err != nil { return err } - r.Status = status + r.Settings = settings return nil } -func (r *Room) SetSettings(settings model.RoomSettings) error { - err := db.SaveRoomSettings(r.ID, settings) +func (r *Room) UpdateSettings(settings map[string]any) error { + rs, err := db.UpdateRoomSettings(r.ID, settings) if err != nil { return err } - r.Settings = settings + r.Settings = rs return nil } + +func (r *Room) ResetMemberPermissions(userID string) error { + return r.SetMemberPermissions(userID, r.Settings.UserDefaultPermissions) +} + +func (r *Room) SetMemberPermissions(userID string, permissions model.RoomMemberPermission) error { + return db.SetMemberPermissions(r.ID, userID, permissions) +} + +func (r *Room) AddMemberPermissions(userID string, permissions model.RoomMemberPermission) error { + return db.AddMemberPermissions(r.ID, userID, permissions) +} + +func (r *Room) RemoveMemberPermissions(userID string, permissions model.RoomMemberPermission) error { + return db.RemoveMemberPermissions(r.ID, userID, permissions) +} + +func (r *Room) ApprovePendingMember(userID string) error { + return db.RoomApprovePendingMember(r.ID, userID) +} + +func (r *Room) BanMember(userID string) error { + return db.RoomBanMember(r.ID, userID) +} + +func (r *Room) UnbanMember(userID string) error { + return db.RoomUnbanMember(r.ID, userID) +} + +func (r *Room) ResetAdminPermissions(userID string) error { + return r.SetAdminPermissions(userID, model.DefaultAdminPermissions) +} + +func (r *Room) SetAdminPermissions(userID string, permissions model.RoomAdminPermission) error { + return db.RoomSetAdminPermissions(r.ID, userID, permissions) +} + +func (r *Room) AddAdminPermissions(userID string, permissions model.RoomAdminPermission) error { + return db.RoomSetAdminPermissions(r.ID, userID, permissions) +} + +func (r *Room) RemoveAdminPermissions(userID string, permissions model.RoomAdminPermission) error { + return db.RoomSetAdminPermissions(r.ID, userID, 0) +} + +func (r *Room) SetAdmin(userID string, permissions model.RoomAdminPermission) error { + return db.RoomSetAdmin(r.ID, userID, permissions) +} + +func (r *Room) SetMember(userID string, permissions model.RoomMemberPermission) error { + return db.RoomSetMember(r.ID, userID, permissions) +} diff --git a/internal/op/user.go b/internal/op/user.go index 66f03db..a0a850b 100644 --- a/internal/op/user.go +++ b/internal/op/user.go @@ -124,7 +124,7 @@ func (u *User) NewMovie(movie *model.BaseMovie) (*model.Movie, error) { } func (u *User) AddMovieToRoom(room *Room, movie *model.BaseMovie) error { - if !u.HasRoomPermission(room, model.PermissionCreateMovie) { + if !u.HasRoomPermission(room, model.PermissionAddMovie) { return model.ErrNoPermission } m, err := u.NewMovie(movie) @@ -157,7 +157,7 @@ func (u *User) NewMovies(movies []*model.BaseMovie) ([]*model.Movie, error) { } func (u *User) AddMoviesToRoom(room *Room, movies []*model.BaseMovie) error { - if !u.HasRoomPermission(room, model.PermissionCreateMovie) { + if !u.HasRoomPermission(room, model.PermissionAddMovie) { return model.ErrNoPermission } m, err := u.NewMovies(movies) @@ -193,22 +193,40 @@ func (u *User) IsPending() bool { return u.Role == model.RolePending } -func (u *User) HasRoomPermission(room *Room, permission model.RoomUserPermission) bool { +func (u *User) HasRoomPermission(room *Room, permission model.RoomMemberPermission) bool { if u.IsAdmin() { return true } return room.HasPermission(u.ID, permission) } +func (u *User) HasRoomAdminPermission(room *Room, permission model.RoomAdminPermission) bool { + if u.IsAdmin() { + return true + } + return room.HasAdminPermission(u.ID, permission) +} + +func (u *User) IsRoomAdmin(room *Room) bool { + if u.IsAdmin() { + return true + } + return room.IsAdmin(u.ID) +} + +func (u *User) IsRoomCreator(room *Room) bool { + return room.CreatorID == u.ID +} + func (u *User) DeleteRoom(room *RoomEntry) error { - if !u.HasRoomPermission(room.Value(), model.PermissionEditRoom) { + if !u.HasRoomAdminPermission(room.Value(), model.PermissionDeleteRoom) { return model.ErrNoPermission } return CompareAndDeleteRoom(room) } func (u *User) SetRoomPassword(room *Room, password string) error { - if !u.HasRoomPermission(room, model.PermissionEditRoom) { + if !u.HasRoomAdminPermission(room, model.PermissionSetRoomPassword) { return model.ErrNoPermission } if !u.IsAdmin() && password == "" && settings.RoomMustNeedPwd.Get() { @@ -238,7 +256,7 @@ func (u *User) UpdateMovie(room *Room, movieID string, movie *model.BaseMovie) e if err != nil { return err } - if m.Movie.CreatorID != u.ID && !u.HasRoomPermission(room, model.PermissionEditUser) { + if m.Movie.CreatorID != u.ID && !u.HasRoomPermission(room, model.PermissionEditMovie) { return model.ErrNoPermission } err = room.UpdateMovie(movieID, movie) @@ -254,19 +272,26 @@ func (u *User) UpdateMovie(room *Room, movieID string, movie *model.BaseMovie) e }) } -func (u *User) SetRoomSetting(room *Room, setting model.RoomSettings) error { - if !u.HasRoomPermission(room, model.PermissionEditRoom) { +func (u *User) SetRoomSetting(room *Room, setting *model.RoomSettings) error { + if !u.HasRoomAdminPermission(room, model.PermissionSetRoomSettings) { return model.ErrNoPermission } return room.SetSettings(setting) } +func (u *User) UpdateRoomSettings(room *Room, settings map[string]interface{}) error { + if !u.HasRoomAdminPermission(room, model.PermissionSetRoomSettings) { + return model.ErrNoPermission + } + return room.UpdateSettings(settings) +} + func (u *User) DeleteMovieByID(room *Room, movieID string) error { m, err := room.GetMovieByID(movieID) if err != nil { return err } - if m.Movie.CreatorID != u.ID && !u.HasRoomPermission(room, model.PermissionEditUser) { + if m.Movie.CreatorID != u.ID && !u.HasRoomPermission(room, model.PermissionDeleteMovie) { return model.ErrNoPermission } return room.DeleteMovieByID(movieID) @@ -278,7 +303,7 @@ func (u *User) DeleteMoviesByID(room *Room, movieIDs []string) error { if err != nil { return err } - if m.Movie.CreatorID != u.ID && !u.HasRoomPermission(room, model.PermissionEditUser) { + if m.Movie.CreatorID != u.ID && !u.HasRoomPermission(room, model.PermissionDeleteMovie) { return model.ErrNoPermission } } @@ -295,7 +320,7 @@ func (u *User) DeleteMoviesByID(room *Room, movieIDs []string) error { } func (u *User) ClearMovies(room *Room) error { - if !u.HasRoomPermission(room, model.PermissionEditRoom) { + if !u.HasRoomPermission(room, model.PermissionDeleteMovie) { return model.ErrNoPermission } err := room.ClearMovies() @@ -312,7 +337,7 @@ func (u *User) ClearMovies(room *Room) error { } func (u *User) SwapMoviePositions(room *Room, id1, id2 string) error { - if !u.HasRoomPermission(room, model.PermissionEditRoom) { + if !u.HasRoomPermission(room, model.PermissionEditMovie) { return model.ErrNoPermission } err := room.SwapMoviePositions(id1, id2) @@ -329,7 +354,7 @@ func (u *User) SwapMoviePositions(room *Room, id1, id2 string) error { } func (u *User) SetCurrentMovie(room *Room, movieID string, play bool) error { - if !u.HasRoomPermission(room, model.PermissionEditCurrent) { + if !u.HasRoomPermission(room, model.PermissionSetCurrentMovie) { return model.ErrNoPermission } err := room.SetCurrentMovie(movieID, play) @@ -403,3 +428,115 @@ func (u *User) VerifyRetrievePasswordCaptchaEmail(e, captcha string) (bool, erro } return email.VerifyRetrievePasswordCaptchaEmail(u.ID, e, captcha) } + +func (u *User) GetRoomMoviesWithPage(room *Room, page, pageSize int) ([]*Movie, int) { + if u.HasRoomPermission(room, model.PermissionGetMovieList) { + return room.GetMoviesWithPage(page, pageSize, "") + } + return room.GetMoviesWithPage(page, pageSize, u.ID) +} + +func (u *User) SetRoomCurrentSeekRate(room *Room, seek, rate, timeDiff float64) (*Status, error) { + if !u.HasRoomPermission(room, model.PermissionSetCurrentStatus) { + return nil, model.ErrNoPermission + } + return room.SetCurrentSeekRate(seek, rate, timeDiff), nil +} + +func (u *User) SetRoomCurrentStatus(room *Room, playing bool, seek, rate, timeDiff float64) (*Status, error) { + if !u.HasRoomPermission(room, model.PermissionSetCurrentStatus) { + return nil, model.ErrNoPermission + } + return room.SetCurrentStatus(playing, seek, rate, timeDiff), nil +} + +func (u *User) BanRoomMember(room *Room, userID string) error { + if !u.HasRoomAdminPermission(room, model.PermissionBanRoomMember) { + return model.ErrNoPermission + } + return room.BanMember(userID) +} + +func (u *User) UnbanRoomMember(room *Room, userID string) error { + if !u.HasRoomAdminPermission(room, model.PermissionBanRoomMember) { + return model.ErrNoPermission + } + return room.UnbanMember(userID) +} + +func (u *User) SetMemberPermissions(room *Room, userID string, permissions model.RoomMemberPermission) error { + if !u.HasRoomAdminPermission(room, model.PermissionSetUserPermission) { + return model.ErrNoPermission + } + return room.SetMemberPermissions(userID, permissions) +} + +func (u *User) AddMemberPermissions(room *Room, userID string, permissions model.RoomMemberPermission) error { + if !u.HasRoomAdminPermission(room, model.PermissionSetUserPermission) { + return model.ErrNoPermission + } + return room.AddMemberPermissions(userID, permissions) +} + +func (u *User) RemoveMemberPermissions(room *Room, userID string, permissions model.RoomMemberPermission) error { + if !u.HasRoomAdminPermission(room, model.PermissionSetUserPermission) { + return model.ErrNoPermission + } + return room.RemoveMemberPermissions(userID, permissions) +} + +func (u *User) ResetMemberPermissions(room *Room, userID string) error { + if !u.HasRoomAdminPermission(room, model.PermissionSetUserPermission) { + return model.ErrNoPermission + } + return room.ResetMemberPermissions(userID) +} + +func (u *User) ApproveRoomPendingMember(room *Room, userID string) error { + if !u.HasRoomAdminPermission(room, model.PermissionApprovePendingMember) { + return model.ErrNoPermission + } + return room.ApprovePendingMember(userID) +} + +func (u *User) SetRoomAdmin(room *Room, userID string, permissions model.RoomAdminPermission) error { + if !u.IsRoomCreator(room) { + return model.ErrNoPermission + } + return room.SetAdmin(userID, permissions) +} + +func (u *User) SetRoomMember(room *Room, userID string, permissions model.RoomMemberPermission) error { + if !u.IsRoomCreator(room) { + return model.ErrNoPermission + } + return room.SetMember(userID, permissions) +} + +func (u *User) SetRoomAdminPermissions(room *Room, userID string, permissions model.RoomAdminPermission) error { + if !u.IsRoomCreator(room) { + return model.ErrNoPermission + } + return room.SetAdminPermissions(userID, permissions) +} + +func (u *User) AddRoomAdminPermissions(room *Room, userID string, permissions model.RoomAdminPermission) error { + if !u.IsRoomCreator(room) { + return model.ErrNoPermission + } + return room.AddAdminPermissions(userID, permissions) +} + +func (u *User) RemoveRoomAdminPermissions(room *Room, userID string, permissions model.RoomAdminPermission) error { + if !u.IsRoomCreator(room) { + return model.ErrNoPermission + } + return room.RemoveAdminPermissions(userID, permissions) +} + +func (u *User) ResetRoomAdminPermissions(room *Room, userID string) error { + if !u.IsRoomCreator(room) { + return model.ErrNoPermission + } + return room.ResetAdminPermissions(userID) +} diff --git a/server/handlers/admin.go b/server/handlers/admin.go index 808d651..32047c8 100644 --- a/server/handlers/admin.go +++ b/server/handlers/admin.go @@ -154,17 +154,43 @@ func Users(ctx *gin.Context) { // search mode, all, name, id switch ctx.DefaultQuery("search", "all") { case "all": - scopes = append(scopes, db.WhereUsernameLikeOrIDIn(keyword, db.GerUsersIDByIDLike(keyword))) + ids, err := db.GerUsersIDByIDLike(keyword) + if err != nil { + log.WithError(err).Error("get users id by id like error") + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) + return + } + scopes = append(scopes, db.WhereUsernameLikeOrIDIn(keyword, ids)) case "name": scopes = append(scopes, db.WhereUsernameLike(keyword)) case "id": - scopes = append(scopes, db.WhereIDIn(db.GerUsersIDByIDLike(keyword))) + ids, err := db.GerUsersIDByIDLike(keyword) + if err != nil { + log.WithError(err).Error("get users id by id like error") + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) + return + } + scopes = append(scopes, db.WhereIDIn(ids)) } } + total, err := db.GetAllUserCount(scopes...) + if err != nil { + log.WithError(err).Error("get all user count error") + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) + return + } + + list, err := db.GetAllUsers(append(scopes, db.Paginate(page, pageSize))...) + if err != nil { + log.WithError(err).Error("get all users error") + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) + return + } + ctx.JSON(http.StatusOK, model.NewApiDataResp(gin.H{ - "total": db.GetAllUserCount(scopes...), - "list": genUserListResp(db.GetAllUsers(append(scopes, db.Paginate(page, pageSize))...)), + "total": total, + "list": genUserListResp(list), })) } @@ -181,31 +207,32 @@ func genUserListResp(us []*dbModel.User) []*model.UserInfoResp { return resp } -func GetRoomUsers(ctx *gin.Context) { +func AdminGetRoomMembers(ctx *gin.Context) { + room := ctx.MustGet("room").(*op.RoomEntry).Value() log := ctx.MustGet("log").(*logrus.Entry) - id := ctx.Query("id") - if len(id) != 32 { - log.Error("room id error") - ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("room id error")) - return - } - page, pageSize, err := utils.GetPageAndMax(ctx) if err != nil { - log.WithError(err).Error("get page and max error") + log.Errorf("get room users failed: %v", err) ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) return } var desc = ctx.DefaultQuery("order", "desc") == "desc" - scopes := []func(db *gorm.DB) *gorm.DB{ - db.PreloadRoomUserRelations(db.WhereRoomID(id)), + scopes := []func(db *gorm.DB) *gorm.DB{} + + switch ctx.DefaultQuery("status", "active") { + case "pending": + scopes = append(scopes, db.WhereRoomMemberStatus(dbModel.RoomMemberStatusPending)) + case "banned": + scopes = append(scopes, db.WhereRoomMemberStatus(dbModel.RoomMemberStatusBanned)) + case "active": + scopes = append(scopes, db.WhereRoomMemberStatus(dbModel.RoomMemberStatusActive)) } switch ctx.DefaultQuery("sort", "name") { - case "createdAt": + case "join": if desc { scopes = append(scopes, db.OrderByCreatedAtDesc) } else { @@ -218,7 +245,7 @@ func GetRoomUsers(ctx *gin.Context) { scopes = append(scopes, db.OrderByAsc("username")) } default: - log.Error("not support sort") + log.Errorf("get room users failed: not support sort") ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("not support sort")) return } @@ -227,31 +254,60 @@ func GetRoomUsers(ctx *gin.Context) { // search mode, all, name, id switch ctx.DefaultQuery("search", "all") { case "all": - scopes = append(scopes, db.WhereUsernameLikeOrIDIn(keyword, db.GerUsersIDByIDLike(keyword))) + ids, err := db.GerUsersIDByIDLike(keyword) + if err != nil { + log.Errorf("get room users failed: %v", err) + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + scopes = append(scopes, db.WhereUsernameLikeOrIDIn(keyword, ids)) case "name": scopes = append(scopes, db.WhereUsernameLike(keyword)) case "id": - scopes = append(scopes, db.WhereIDIn(db.GerUsersIDByIDLike(keyword))) + ids, err := db.GerUsersIDByIDLike(keyword) + if err != nil { + log.Errorf("get room users failed: %v", err) + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + scopes = append(scopes, db.WhereIDIn(ids)) } } + scopes = append(scopes, func(db *gorm.DB) *gorm.DB { + return db.InnerJoins("JOIN room_members ON users.id = room_members.user_id AND room_members.room_id = ?", room.ID) + }, db.PreloadRoomMembers()) + + total, err := db.GetAllUserCount(scopes...) + if err != nil { + log.Errorf("get room users failed: %v", err) + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + + list, err := db.GetAllUsers(append(scopes, db.Paginate(page, pageSize))...) + if err != nil { + log.Errorf("get room users failed: %v", err) + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } ctx.JSON(http.StatusOK, model.NewApiDataResp(gin.H{ - "total": db.GetAllUserCount(scopes...), - "list": genRoomUserListResp(db.GetAllUsers(append(scopes, db.Paginate(page, pageSize))...)), + "total": total, + "list": genRoomMemberListResp(list), })) } -func genRoomUserListResp(us []*dbModel.User) []*model.RoomUsersResp { - resp := make([]*model.RoomUsersResp, len(us)) +func genRoomMemberListResp(us []*dbModel.User) []*model.RoomMembersResp { + resp := make([]*model.RoomMembersResp, len(us)) for i, v := range us { - resp[i] = &model.RoomUsersResp{ - UserID: v.ID, - Username: v.Username, - Role: v.Role, - JoinAt: v.RoomUserRelations[0].CreatedAt.UnixMilli(), - RoomID: v.RoomUserRelations[0].RoomID, - Status: v.RoomUserRelations[0].Status, - Permissions: v.RoomUserRelations[0].Permissions, + resp[i] = &model.RoomMembersResp{ + UserID: v.ID, + Username: v.Username, + JoinAt: v.RoomMembers[0].CreatedAt.UnixMilli(), + Role: v.RoomMembers[0].Role, + RoomID: v.RoomMembers[0].RoomID, + Permissions: v.RoomMembers[0].Permissions, + AdminPermissions: v.RoomMembers[0].AdminPermissions, } } return resp @@ -409,11 +465,23 @@ func Rooms(ctx *gin.Context) { // search mode, all, name, creator switch ctx.DefaultQuery("search", "all") { case "all": - scopes = append(scopes, db.WhereRoomNameLikeOrCreatorInOrIDLike(keyword, db.GerUsersIDByUsernameLike(keyword), keyword)) + ids, err := db.GerUsersIDByUsernameLike(keyword) + if err != nil { + log.WithError(err).Error("get users id by username like error") + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) + return + } + scopes = append(scopes, db.WhereRoomNameLikeOrCreatorInOrIDLike(keyword, ids, keyword)) case "name": scopes = append(scopes, db.WhereRoomNameLike(keyword)) case "creator": - scopes = append(scopes, db.WhereCreatorIDIn(db.GerUsersIDByUsernameLike(keyword))) + ids, err := db.GerUsersIDByUsernameLike(keyword) + if err != nil { + log.WithError(err).Error("get users id by username like error") + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) + return + } + scopes = append(scopes, db.WhereCreatorIDIn(ids)) case "creatorId": scopes = append(scopes, db.WhereCreatorID(keyword)) case "id": @@ -421,9 +489,23 @@ func Rooms(ctx *gin.Context) { } } + total, err := db.GetAllRoomsCount(scopes...) + if err != nil { + log.WithError(err).Error("get all rooms count error") + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) + return + } + + list, err := genRoomListResp(append(scopes, db.Paginate(page, pageSize))...) + if err != nil { + log.WithError(err).Error("gen room list resp error") + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) + return + } + ctx.JSON(http.StatusOK, model.NewApiDataResp(gin.H{ - "total": db.GetAllRoomsCount(scopes...), - "list": genRoomListResp(append(scopes, db.Paginate(page, pageSize))...), + "total": total, + "list": list, })) } @@ -489,9 +571,24 @@ func GetUserRooms(ctx *gin.Context) { } } + total, err := db.GetAllRoomsCount(scopes...) + if err != nil { + log.WithError(err).Error("get all rooms count error") + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) + return + } + + list, err := genRoomListResp(append(scopes, db.Paginate(page, pageSize))...) + if err != nil { + log.WithError(err).Error("gen room list resp error") + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) + return + + } + ctx.JSON(http.StatusOK, model.NewApiDataResp(gin.H{ - "total": db.GetAllRoomsCount(scopes...), - "list": genRoomListResp(append(scopes, db.Paginate(page, pageSize))...), + "total": total, + "list": list, })) } diff --git a/server/handlers/init.go b/server/handlers/init.go index 5fda669..220b932 100644 --- a/server/handlers/init.go +++ b/server/handlers/init.go @@ -147,7 +147,7 @@ func initAdmin(admin *gin.RouterGroup, root *gin.RouterGroup) { room.POST("/unban", UnBanRoom) - room.GET("/users", GetRoomUsers) + room.GET("/members", AdminGetRoomMembers) } } @@ -171,19 +171,42 @@ func initRoom(room *gin.RouterGroup, needAuthUser *gin.RouterGroup, needAuthRoom needAuthUser.POST("/login", LoginRoom) - needAuthRoom.POST("/delete", DeleteRoom) + needAuthRoom.GET("/me", RoomMe) - needAuthRoom.POST("/pwd", SetRoomPassword) + needAuthRoom.GET("/members", RoomMembers) - needAuthRoom.GET("/settings", RoomSetting) + { + needAuthRoomAdmin := needAuthRoom.Group("/admin", middlewares.AuthRoomAdminMiddleware) + needAuthRoomCreator := needAuthRoom.Group("/admin", middlewares.AuthRoomCreatorMiddleware) + + needAuthRoomAdmin.GET("/settings", RoomSetting) + + needAuthRoomAdmin.POST("/settings", SetRoomSetting) + + needAuthRoomAdmin.POST("/delete", DeleteRoom) + + needAuthRoomAdmin.POST("/pwd", SetRoomPassword) + + needAuthRoomAdmin.GET("/members", RoomAdminMembers) - needAuthRoom.POST("/settings", SetRoomSetting) + needAuthRoomAdmin.POST("/members/approve", RoomAdminApproveMember) - needAuthRoom.GET("/users", RoomUsers) + needAuthRoomAdmin.POST("/members/ban", RoomAdminBanMember) + + needAuthRoomAdmin.POST("/members/unban", RoomAdminUnbanMember) + + needAuthRoomCreator.POST("/members/permissions", RoomSetMemberPermissions) + + needAuthRoomCreator.POST("/members", RoomSetMember) + + needAuthRoomCreator.POST("/members/admin", RoomSetAdmin) + + needAuthRoomCreator.POST("/members/admin/permissions", RoomSetAdminPermissions) + } } func initMovie(movie *gin.RouterGroup, needAuthMovie *gin.RouterGroup) { - needAuthMovie.GET("/list", MovieList) + // needAuthMovie.GET("/list", MovieList) needAuthMovie.GET("/current", CurrentMovie) diff --git a/server/handlers/member.go b/server/handlers/member.go new file mode 100644 index 0000000..fbfbaee --- /dev/null +++ b/server/handlers/member.go @@ -0,0 +1,339 @@ +package handlers + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" + "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" + "github.com/synctv-org/synctv/utils" + "gorm.io/gorm" +) + +func RoomMembers(ctx *gin.Context) { + room := ctx.MustGet("room").(*op.RoomEntry).Value() + log := ctx.MustGet("log").(*logrus.Entry) + + page, pageSize, err := utils.GetPageAndMax(ctx) + if err != nil { + log.Errorf("get room users failed: %v", err) + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) + return + } + + var desc = ctx.DefaultQuery("order", "desc") == "desc" + + scopes := []func(db *gorm.DB) *gorm.DB{} + + switch ctx.DefaultQuery("sort", "name") { + case "join": + 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")) + } + default: + log.Errorf("get room users failed: not support sort") + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("not support sort")) + return + } + + if keyword := ctx.Query("keyword"); keyword != "" { + // search mode, all, name, id + switch ctx.DefaultQuery("search", "all") { + case "all": + ids, err := db.GerUsersIDByIDLike(keyword) + if err != nil { + log.Errorf("get room users failed: %v", err) + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + scopes = append(scopes, db.WhereUsernameLikeOrIDIn(keyword, ids)) + case "name": + scopes = append(scopes, db.WhereUsernameLike(keyword)) + case "id": + ids, err := db.GerUsersIDByIDLike(keyword) + if err != nil { + log.Errorf("get room users failed: %v", err) + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + scopes = append(scopes, db.WhereIDIn(ids)) + } + } + scopes = append(scopes, func(db *gorm.DB) *gorm.DB { + return db.InnerJoins("JOIN room_members ON users.id = room_members.user_id AND room_members.room_id = ?", room.ID) + }, db.PreloadRoomMembers()) + + total, err := db.GetAllUserCount(scopes...) + if err != nil { + log.Errorf("get room users failed: %v", err) + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + + list, err := db.GetAllUsers(append(scopes, db.Paginate(page, pageSize))...) + if err != nil { + log.Errorf("get room users failed: %v", err) + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + + ctx.JSON(http.StatusOK, model.NewApiDataResp(gin.H{ + "total": total, + "list": genRoomMemberListResp(list), + })) +} + +func RoomAdminMembers(ctx *gin.Context) { + room := ctx.MustGet("room").(*op.RoomEntry).Value() + log := ctx.MustGet("log").(*logrus.Entry) + + page, pageSize, err := utils.GetPageAndMax(ctx) + if err != nil { + log.Errorf("get room users failed: %v", err) + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) + return + } + + var desc = ctx.DefaultQuery("order", "desc") == "desc" + + scopes := []func(db *gorm.DB) *gorm.DB{} + + switch ctx.DefaultQuery("status", "active") { + case "pending": + scopes = append(scopes, db.WhereRoomMemberStatus(dbModel.RoomMemberStatusPending)) + case "banned": + scopes = append(scopes, db.WhereRoomMemberStatus(dbModel.RoomMemberStatusBanned)) + case "active": + scopes = append(scopes, db.WhereRoomMemberStatus(dbModel.RoomMemberStatusActive)) + } + + switch ctx.DefaultQuery("sort", "name") { + case "join": + 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")) + } + default: + log.Errorf("get room users failed: not support sort") + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("not support sort")) + return + } + + if keyword := ctx.Query("keyword"); keyword != "" { + // search mode, all, name, id + switch ctx.DefaultQuery("search", "all") { + case "all": + ids, err := db.GerUsersIDByIDLike(keyword) + if err != nil { + log.Errorf("get room users failed: %v", err) + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + scopes = append(scopes, db.WhereUsernameLikeOrIDIn(keyword, ids)) + case "name": + scopes = append(scopes, db.WhereUsernameLike(keyword)) + case "id": + ids, err := db.GerUsersIDByIDLike(keyword) + if err != nil { + log.Errorf("get room users failed: %v", err) + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + scopes = append(scopes, db.WhereIDIn(ids)) + } + } + scopes = append(scopes, func(db *gorm.DB) *gorm.DB { + return db.InnerJoins("JOIN room_members ON users.id = room_members.user_id AND room_members.room_id = ?", room.ID) + }, db.PreloadRoomMembers()) + + total, err := db.GetAllUserCount(scopes...) + if err != nil { + log.Errorf("get room users failed: %v", err) + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + + list, err := db.GetAllUsers(append(scopes, db.Paginate(page, pageSize))...) + if err != nil { + log.Errorf("get room users failed: %v", err) + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + + ctx.JSON(http.StatusOK, model.NewApiDataResp(gin.H{ + "total": total, + "list": genRoomMemberListResp(list), + })) +} + +func RoomAdminApproveMember(ctx *gin.Context) { + user := ctx.MustGet("user").(*op.UserEntry).Value() + room := ctx.MustGet("room").(*op.RoomEntry).Value() + log := ctx.MustGet("log").(*logrus.Entry) + + var req model.RoomApproveMemberReq + if err := model.Decode(ctx, &req); err != nil { + log.Errorf("decode room approve user req failed: %v", err) + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) + return + } + + err := user.ApproveRoomPendingMember(room, req.ID) + if err != nil { + log.Errorf("approve room user failed: %v", err) + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + + ctx.Status(http.StatusNoContent) +} + +func RoomAdminBanMember(ctx *gin.Context) { + user := ctx.MustGet("user").(*op.UserEntry).Value() + room := ctx.MustGet("room").(*op.RoomEntry).Value() + log := ctx.MustGet("log").(*logrus.Entry) + + var req model.RoomBanMemberReq + if err := model.Decode(ctx, &req); err != nil { + log.Errorf("decode room ban user req failed: %v", err) + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) + return + } + + err := user.BanRoomMember(room, req.ID) + if err != nil { + log.Errorf("ban room user failed: %v", err) + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + + ctx.Status(http.StatusNoContent) +} + +func RoomAdminUnbanMember(ctx *gin.Context) { + user := ctx.MustGet("user").(*op.UserEntry).Value() + room := ctx.MustGet("room").(*op.RoomEntry).Value() + log := ctx.MustGet("log").(*logrus.Entry) + + var req model.RoomUnbanMemberReq + if err := model.Decode(ctx, &req); err != nil { + log.Errorf("decode room unban user req failed: %v", err) + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) + return + } + + err := user.UnbanRoomMember(room, req.ID) + if err != nil { + log.Errorf("unban room user failed: %v", err) + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + + ctx.Status(http.StatusNoContent) +} + +func RoomSetMemberPermissions(ctx *gin.Context) { + user := ctx.MustGet("user").(*op.UserEntry).Value() + room := ctx.MustGet("room").(*op.RoomEntry).Value() + log := ctx.MustGet("log").(*logrus.Entry) + + var req model.RoomSetMemberPermissionsReq + if err := model.Decode(ctx, &req); err != nil { + log.Errorf("decode room set user permissions req failed: %v", err) + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) + return + } + + err := user.SetMemberPermissions(room, req.ID, req.Permissions) + if err != nil { + log.Errorf("set room user permissions failed: %v", err) + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + + ctx.Status(http.StatusNoContent) +} + +func RoomSetAdmin(ctx *gin.Context) { + user := ctx.MustGet("user").(*op.UserEntry).Value() + room := ctx.MustGet("room").(*op.RoomEntry).Value() + log := ctx.MustGet("log").(*logrus.Entry) + + var req model.RoomSetAdminReq + if err := model.Decode(ctx, &req); err != nil { + log.Errorf("decode room set admin req failed: %v", err) + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) + return + } + + err := user.SetRoomAdmin(room, req.ID, req.AdminPermissions) + if err != nil { + log.Errorf("set room admin failed: %v", err) + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + + ctx.Status(http.StatusNoContent) +} + +func RoomSetMember(ctx *gin.Context) { + user := ctx.MustGet("user").(*op.UserEntry).Value() + room := ctx.MustGet("room").(*op.RoomEntry).Value() + log := ctx.MustGet("log").(*logrus.Entry) + + var req model.RoomSetMemberReq + if err := model.Decode(ctx, &req); err != nil { + log.Errorf("decode room set user req failed: %v", err) + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) + return + } + + err := user.SetRoomMember(room, req.ID, req.Permissions) + if err != nil { + log.Errorf("set room user failed: %v", err) + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + + ctx.Status(http.StatusNoContent) +} + +func RoomSetAdminPermissions(ctx *gin.Context) { + user := ctx.MustGet("user").(*op.UserEntry).Value() + room := ctx.MustGet("room").(*op.RoomEntry).Value() + log := ctx.MustGet("log").(*logrus.Entry) + + var req model.RoomSetAdminPermissionsReq + if err := model.Decode(ctx, &req); err != nil { + log.Errorf("decode room set admin permissions req failed: %v", err) + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) + return + } + + err := user.SetRoomAdminPermissions(room, req.ID, req.AdminPermissions) + if err != nil { + log.Errorf("set room admin permissions failed: %v", err) + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + + ctx.Status(http.StatusNoContent) +} diff --git a/server/handlers/movie.go b/server/handlers/movie.go index 72e0342..ead16de 100644 --- a/server/handlers/movie.go +++ b/server/handlers/movie.go @@ -39,46 +39,46 @@ func GetPageItems[T any](ctx *gin.Context, items []T) ([]T, error) { return utils.GetPageItems(items, page, max), nil } -func MovieList(ctx *gin.Context) { - room := ctx.MustGet("room").(*op.RoomEntry).Value() - user := ctx.MustGet("user").(*op.UserEntry).Value() - log := ctx.MustGet("log").(*logrus.Entry) - - page, max, err := utils.GetPageAndMax(ctx) - if err != nil { - log.Errorf("get page and max error: %v", err) - ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) - return - } +// func MovieList(ctx *gin.Context) { +// room := ctx.MustGet("room").(*op.RoomEntry).Value() +// user := ctx.MustGet("user").(*op.UserEntry).Value() +// log := ctx.MustGet("log").(*logrus.Entry) + +// page, max, err := utils.GetPageAndMax(ctx) +// if err != nil { +// log.Errorf("get page and max error: %v", err) +// ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) +// return +// } - currentResp, err := genCurrentResp(ctx, user, room) - if err != nil { - log.Errorf("gen current resp error: %v", err) - ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) - return - } +// currentResp, err := genCurrentResp(ctx, user, room) +// if err != nil { +// log.Errorf("gen current resp error: %v", err) +// ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) +// return +// } - m := room.GetMoviesWithPage(page, max) - mresp := make([]model.MovieResp, len(m)) - for i, v := range m { - mresp[i] = model.MovieResp{ - Id: v.Movie.ID, - Base: v.Movie.Base, - Creator: op.GetUserName(v.Movie.CreatorID), - } - // hide url and headers when proxy - if user.ID != v.Movie.CreatorID && v.Movie.Base.Proxy { - mresp[i].Base.Url = "" - mresp[i].Base.Headers = nil - } - } +// m := room.GetMoviesWithPage(page, max) +// mresp := make([]model.MovieResp, len(m)) +// for i, v := range m { +// mresp[i] = model.MovieResp{ +// Id: v.Movie.ID, +// Base: v.Movie.Base, +// Creator: op.GetUserName(v.Movie.CreatorID), +// } +// // hide url and headers when proxy +// if user.ID != v.Movie.CreatorID && v.Movie.Base.Proxy { +// mresp[i].Base.Url = "" +// mresp[i].Base.Headers = nil +// } +// } - ctx.JSON(http.StatusOK, model.NewApiDataResp(gin.H{ - "current": currentResp, - "total": room.GetMoviesCount(), - "movies": mresp, - })) -} +// ctx.JSON(http.StatusOK, model.NewApiDataResp(gin.H{ +// "current": currentResp, +// "total": room.GetMoviesCount(), +// "movies": mresp, +// })) +// } func genCurrentResp(ctx context.Context, user *op.User, room *op.Room) (*model.CurrentMovieResp, error) { return genCurrentRespWithCurrent(ctx, user, room, room.Current()) @@ -88,7 +88,7 @@ func genCurrentMovieInfo(ctx context.Context, user *op.User, room *op.Room, opMo if opMovie == nil || opMovie.ID == "" { return &model.MovieResp{}, nil } - var movie = opMovie.Movie + var movie = *opMovie.Movie if movie.Base.VendorInfo.Vendor != "" { vendorMovie, err := genVendorMovie(ctx, user, opMovie) if err != nil { @@ -171,7 +171,7 @@ func Movies(ctx *gin.Context) { return } - m := room.GetMoviesWithPage(int(page), int(max)) + m, total := user.GetRoomMoviesWithPage(room, int(page), int(max)) mresp := make([]*model.MovieResp, len(m)) for i, v := range m { @@ -188,7 +188,7 @@ func Movies(ctx *gin.Context) { } ctx.JSON(http.StatusOK, model.NewApiDataResp(gin.H{ - "total": room.GetMoviesCount(), + "total": total, "movies": mresp, })) } @@ -276,7 +276,7 @@ func NewPublishKey(ctx *gin.Context) { return } - if movie.Movie.CreatorID != user.ID && !user.HasRoomPermission(room, dbModel.PermissionEditUser) { + if movie.Movie.CreatorID != user.ID { log.Errorf("new publish key error: %v", dbModel.ErrNoPermission) ctx.AbortWithStatusJSON(http.StatusForbidden, model.NewApiErrorResp(dbModel.ErrNoPermission)) return @@ -1057,7 +1057,7 @@ func proxyVendorMovie(ctx *gin.Context, movie *op.Movie) { // user is the api requester func genVendorMovie(ctx context.Context, user *op.User, opMovie *op.Movie) (*dbModel.Movie, error) { - movie := opMovie.Movie + movie := *opMovie.Movie var err error switch movie.Base.VendorInfo.Vendor { case dbModel.VendorBilibili: diff --git a/server/handlers/room.go b/server/handlers/room.go index 37ab58e..f3f7ec3 100644 --- a/server/handlers/room.go +++ b/server/handlers/room.go @@ -3,7 +3,6 @@ package handlers import ( "context" "errors" - "fmt" "net/http" "slices" "time" @@ -29,10 +28,26 @@ var ( ErrRoomAlready = errors.New("room already exists") ) -type FormatErrNotSupportPosition string +func RoomMe(ctx *gin.Context) { + user := ctx.MustGet("user").(*op.UserEntry).Value() + room := ctx.MustGet("room").(*op.RoomEntry).Value() + log := ctx.MustGet("log").(*logrus.Entry) + + rur, err := room.LoadOrCreateRoomMember(user.ID) + if err != nil { + log.Errorf("room me failed: %v", err) + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } -func (e FormatErrNotSupportPosition) Error() string { - return fmt.Sprintf("not support position %s", string(e)) + ctx.JSON(http.StatusOK, model.NewApiDataResp(&model.RoomMeResp{ + UserID: user.ID, + RoomID: room.ID, + JoinAt: rur.CreatedAt.UnixMilli(), + Role: rur.Role, + Permissions: rur.Permissions, + AdminPermissions: rur.AdminPermissions, + })) } func CreateRoom(ctx *gin.Context) { @@ -52,7 +67,7 @@ func CreateRoom(ctx *gin.Context) { return } - room, err := user.CreateRoom(req.RoomName, req.Password, db.WithSetting(req.Setting)) + room, err := user.CreateRoom(req.RoomName, req.Password, db.WithSettingHidden(req.Hidden)) if err != nil { log.Errorf("create room failed: %v", err) ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) @@ -72,7 +87,7 @@ func CreateRoom(ctx *gin.Context) { })) } -var roomHotCache = refreshcache.NewRefreshCache[[]*model.RoomListResp](func(context.Context, ...any) ([]*model.RoomListResp, error) { +var roomHotCache = refreshcache.NewRefreshCache(func(context.Context, ...any) ([]*model.RoomListResp, error) { rooms := make([]*model.RoomListResp, 0) op.RangeRoomCache(func(key string, value *synccache.Entry[*op.Room]) bool { v := value.Value() @@ -145,6 +160,9 @@ func RoomList(ctx *gin.Context) { var desc = ctx.DefaultQuery("order", "desc") == "desc" scopes := []func(db *gorm.DB) *gorm.DB{ + func(db *gorm.DB) *gorm.DB { + return db.InnerJoins("JOIN room_settings ON rooms.id = room_settings.id") + }, db.WhereRoomSettingWithoutHidden(), db.WhereStatus(dbModel.RoomStatusActive), } @@ -172,24 +190,53 @@ func RoomList(ctx *gin.Context) { // search mode, all, name, creator switch ctx.DefaultQuery("search", "all") { case "all": - scopes = append(scopes, db.WhereRoomNameLikeOrCreatorInOrIDLike(keyword, db.GerUsersIDByUsernameLike(keyword), keyword)) + ids, err := db.GerUsersIDByUsernameLike(keyword) + if err != nil { + log.Errorf("get room list failed: %v", err) + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + scopes = append(scopes, db.WhereRoomNameLikeOrCreatorInOrIDLike(keyword, ids, keyword)) case "name": scopes = append(scopes, db.WhereRoomNameLike(keyword)) case "creator": - scopes = append(scopes, db.WhereCreatorIDIn(db.GerUsersIDByUsernameLike(keyword))) + ids, err := db.GerUsersIDByUsernameLike(keyword) + if err != nil { + log.Errorf("get room list failed: %v", err) + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + scopes = append(scopes, db.WhereCreatorIDIn(ids)) case "id": scopes = append(scopes, db.WhereIDLike(keyword)) } } + total, err := db.GetAllRoomsCount(scopes...) + if err != nil { + log.Errorf("get room list failed: %v", err) + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + + list, err := genRoomListResp(append(scopes, db.Paginate(page, pageSize))...) + if err != nil { + log.Errorf("get room list failed: %v", err) + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + ctx.JSON(http.StatusOK, model.NewApiDataResp(gin.H{ - "total": db.GetAllRoomsCount(scopes...), - "list": genRoomListResp(append(scopes, db.Paginate(page, pageSize))...), + "total": total, + "list": list, })) } -func genRoomListResp(scopes ...func(db *gorm.DB) *gorm.DB) []*model.RoomListResp { - rs := db.GetAllRooms(scopes...) +func genRoomListResp(scopes ...func(db *gorm.DB) *gorm.DB) ([]*model.RoomListResp, error) { + rs, err := db.GetAllRooms(scopes...) + if err != nil { + return nil, err + } resp := make([]*model.RoomListResp, len(rs)) for i, r := range rs { resp[i] = &model.RoomListResp{ @@ -203,7 +250,7 @@ func genRoomListResp(scopes ...func(db *gorm.DB) *gorm.DB) []*model.RoomListResp Status: r.Status, } } - return resp + return resp, nil } func CheckRoom(ctx *gin.Context) { @@ -336,7 +383,7 @@ func SetRoomSetting(ctx *gin.Context) { return } - if err := user.SetRoomSetting(room, dbModel.RoomSettings(req)); err != nil { + if err := user.UpdateRoomSettings(room, req); err != nil { log.Errorf("set room setting failed: %v", err) if errors.Is(err, dbModel.ErrNoPermission) { ctx.AbortWithStatusJSON(http.StatusForbidden, model.NewApiErrorResp(err)) @@ -348,66 +395,3 @@ func SetRoomSetting(ctx *gin.Context) { ctx.Status(http.StatusNoContent) } - -func RoomUsers(ctx *gin.Context) { - room := ctx.MustGet("room").(*op.RoomEntry).Value() - log := ctx.MustGet("log").(*logrus.Entry) - - page, pageSize, err := utils.GetPageAndMax(ctx) - if err != nil { - log.Errorf("get room users failed: %v", err) - ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) - return - } - - var desc = ctx.DefaultQuery("order", "desc") == "desc" - - preloadScopes := []func(db *gorm.DB) *gorm.DB{db.WhereRoomID(room.ID)} - scopes := []func(db *gorm.DB) *gorm.DB{} - - switch ctx.DefaultQuery("status", "active") { - case "pending": - preloadScopes = append(preloadScopes, db.WhereRoomUserStatus(dbModel.RoomUserStatusPending)) - case "banned": - preloadScopes = append(preloadScopes, db.WhereRoomUserStatus(dbModel.RoomUserStatusBanned)) - case "active": - preloadScopes = append(preloadScopes, db.WhereRoomUserStatus(dbModel.RoomUserStatusActive)) - } - - switch ctx.DefaultQuery("sort", "name") { - case "join": - if desc { - preloadScopes = append(preloadScopes, db.OrderByCreatedAtDesc) - } else { - preloadScopes = append(preloadScopes, db.OrderByCreatedAtAsc) - } - case "name": - if desc { - scopes = append(scopes, db.OrderByDesc("username")) - } else { - scopes = append(scopes, db.OrderByAsc("username")) - } - default: - log.Errorf("get room users failed: not support sort") - ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("not support sort")) - return - } - - if keyword := ctx.Query("keyword"); keyword != "" { - // search mode, all, name, id - switch ctx.DefaultQuery("search", "all") { - case "all": - scopes = append(scopes, db.WhereUsernameLikeOrIDIn(keyword, db.GerUsersIDByIDLike(keyword))) - case "name": - scopes = append(scopes, db.WhereUsernameLike(keyword)) - case "id": - scopes = append(scopes, db.WhereIDIn(db.GerUsersIDByIDLike(keyword))) - } - } - scopes = append(scopes, db.PreloadRoomUserRelations(preloadScopes...)) - - ctx.JSON(http.StatusOK, model.NewApiDataResp(gin.H{ - "total": db.GetAllUserCount(scopes...), - "list": genRoomUserListResp(db.GetAllUsers(append(scopes, db.Paginate(page, pageSize))...)), - })) -} diff --git a/server/handlers/user.go b/server/handlers/user.go index 7766345..2f29086 100644 --- a/server/handlers/user.go +++ b/server/handlers/user.go @@ -138,7 +138,13 @@ func UserRooms(ctx *gin.Context) { // search mode, all, name, creator switch ctx.DefaultQuery("search", "all") { case "all": - scopes = append(scopes, db.WhereRoomNameLikeOrCreatorInOrIDLike(keyword, db.GerUsersIDByUsernameLike(keyword), keyword)) + ids, err := db.GerUsersIDByUsernameLike(keyword) + if err != nil { + log.Errorf("failed to get all rooms count: %v", err) + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + scopes = append(scopes, db.WhereRoomNameLikeOrCreatorInOrIDLike(keyword, ids, keyword)) case "name": scopes = append(scopes, db.WhereRoomNameLike(keyword)) case "id": @@ -146,9 +152,23 @@ func UserRooms(ctx *gin.Context) { } } + total, err := db.GetAllRoomsCount(scopes...) + if err != nil { + log.Errorf("failed to get all rooms count: %v", err) + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + + list, err := genRoomListResp(append(scopes, db.Paginate(page, pageSize))...) + if err != nil { + log.Errorf("failed to get all rooms: %v", err) + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + ctx.JSON(http.StatusOK, model.NewApiDataResp(gin.H{ - "total": db.GetAllRoomsCount(scopes...), - "list": genRoomListResp(append(scopes, db.Paginate(page, pageSize))...), + "total": total, + "list": list, })) } diff --git a/server/handlers/websocket.go b/server/handlers/websocket.go index 92a769f..a44f042 100644 --- a/server/handlers/websocket.go +++ b/server/handlers/websocket.go @@ -1,6 +1,7 @@ package handlers import ( + "errors" "io" "net/http" "time" @@ -9,6 +10,7 @@ import ( "github.com/gorilla/websocket" "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus" + dbModel "github.com/synctv-org/synctv/internal/model" "github.com/synctv-org/synctv/internal/op" pb "github.com/synctv-org/synctv/proto/message" "github.com/synctv-org/synctv/server/middlewares" @@ -133,7 +135,7 @@ func handleReaderMessage(c *op.Client, l *logrus.Entry) error { } l.Debugf("ws: receive message: %v", msg.String()) - if err = handleElementMsg(c, &msg, l); err != nil { + if err = handleElementMsg(c, &msg); err != nil { l.Errorf("ws: handle message error: %v", err) return err } @@ -142,7 +144,7 @@ func handleReaderMessage(c *op.Client, l *logrus.Entry) error { const MaxChatMessageLength = 4096 -func handleElementMsg(cli *op.Client, msg *pb.ElementMessage, l *logrus.Entry) error { +func handleElementMsg(cli *op.Client, msg *pb.ElementMessage) error { var timeDiff float64 if msg.Time != 0 { timeDiff = time.Since(time.UnixMilli(msg.Time)).Seconds() @@ -163,20 +165,24 @@ func handleElementMsg(cli *op.Client, msg *pb.ElementMessage, l *logrus.Entry) e Error: "message too long", }) } - return cli.Broadcast(&pb.ElementMessage{ - Type: pb.ElementMessageType_CHAT_MESSAGE, - ChatResp: &pb.ChatResp{ - Sender: &pb.Sender{ - Username: cli.User().Username, - Userid: cli.User().ID, - }, - Message: message, - }, - }) + err := cli.SendChatMessage(message) + if err != nil && errors.Is(err, dbModel.ErrNoPermission) { + return cli.Send(&pb.ElementMessage{ + Type: pb.ElementMessageType_ERROR, + Error: err.Error(), + }) + } + return err case pb.ElementMessageType_PLAY, pb.ElementMessageType_PAUSE, pb.ElementMessageType_CHANGE_RATE: - status := cli.Room().SetStatus(msg.ChangeMovieStatusReq.Playing, msg.ChangeMovieStatusReq.Seek, msg.ChangeMovieStatusReq.Rate, timeDiff) + status, err := cli.SetStatus(msg.ChangeMovieStatusReq.Playing, msg.ChangeMovieStatusReq.Seek, msg.ChangeMovieStatusReq.Rate, timeDiff) + if err != nil { + return cli.Send(&pb.ElementMessage{ + Type: pb.ElementMessageType_ERROR, + Error: err.Error(), + }) + } return cli.Broadcast(&pb.ElementMessage{ Type: msg.Type, MovieStatusChanged: &pb.MovieStatusChanged{ @@ -192,7 +198,13 @@ func handleElementMsg(cli *op.Client, msg *pb.ElementMessage, l *logrus.Entry) e }, }, op.WithIgnoreClient(cli)) case pb.ElementMessageType_CHANGE_SEEK: - status := cli.Room().SetSeekRate(msg.ChangeMovieStatusReq.Seek, msg.ChangeMovieStatusReq.Rate, timeDiff) + status, err := cli.SetSeekRate(msg.ChangeMovieStatusReq.Seek, msg.ChangeMovieStatusReq.Rate, timeDiff) + if err != nil { + return cli.Send(&pb.ElementMessage{ + Type: pb.ElementMessageType_ERROR, + Error: err.Error(), + }) + } return cli.Broadcast(&pb.ElementMessage{ Type: msg.Type, MovieStatusChanged: &pb.MovieStatusChanged{ diff --git a/server/middlewares/auth.go b/server/middlewares/auth.go index 80728d3..1a84c9e 100644 --- a/server/middlewares/auth.go +++ b/server/middlewares/auth.go @@ -2,6 +2,7 @@ package middlewares import ( "errors" + "fmt" "net/http" "strings" "time" @@ -93,6 +94,17 @@ func AuthRoom(Authorization string) (*op.UserEntry, *op.RoomEntry, error) { return nil, nil, ErrAuthExpired } + rus, err := r.Value().UserStatus(u.Value().ID) + if err != nil { + return nil, nil, err + } + if !rus.IsActive() { + if rus.IsPending() { + return nil, nil, fmt.Errorf("user is pending, need admin to approve") + } + return nil, nil, fmt.Errorf("user is banned") + } + return u, r, nil } @@ -155,10 +167,10 @@ func NewAuthRoomToken(user *op.User, room *op.Room) (string, error) { return "", errors.New("room is pending, need admin to approve") } if room.Settings.DisableJoinNewUser { - if _, err := room.GetRoomUserRelation(user.ID); err != nil { + if _, err := room.GetRoomMemberPermission(user.ID); err != nil { return "", errors.New("room is not allow new user to join") } - } else if _, err := room.LoadOrCreateRoomUserRelation(user.ID); err != nil { + } else if _, err := room.LoadOrCreateRoomMember(user.ID); err != nil { return "", err } @@ -257,6 +269,36 @@ func AuthRoomMiddleware(ctx *gin.Context) { log.Data["uro"] = user.Role.String() } +func AuthRoomAdminMiddleware(ctx *gin.Context) { + AuthRoomMiddleware(ctx) + if ctx.IsAborted() { + return + } + + room := ctx.MustGet("room").(*synccache.Entry[*op.Room]).Value() + user := ctx.MustGet("user").(*synccache.Entry[*op.User]).Value() + + if !user.IsRoomAdmin(room) { + ctx.AbortWithStatusJSON(http.StatusForbidden, model.NewApiErrorStringResp("user has no permission")) + return + } +} + +func AuthRoomCreatorMiddleware(ctx *gin.Context) { + AuthRoomMiddleware(ctx) + if ctx.IsAborted() { + return + } + + room := ctx.MustGet("room").(*synccache.Entry[*op.Room]).Value() + user := ctx.MustGet("user").(*synccache.Entry[*op.User]).Value() + + if room.CreatorID != user.ID { + ctx.AbortWithStatusJSON(http.StatusForbidden, model.NewApiErrorStringResp("user is not creator")) + return + } +} + func AuthAdminMiddleware(ctx *gin.Context) { AuthUserMiddleware(ctx) if ctx.IsAborted() { diff --git a/server/model/member.go b/server/model/member.go new file mode 100644 index 0000000..cfed6cf --- /dev/null +++ b/server/model/member.go @@ -0,0 +1,48 @@ +package model + +import ( + dbModel "github.com/synctv-org/synctv/internal/model" +) + +type RoomMembersResp struct { + UserID string `json:"userId"` + Username string `json:"username"` + JoinAt int64 `json:"joinAt"` + Role dbModel.RoomMemberRole `json:"role"` + RoomID string `json:"roomId"` + Permissions dbModel.RoomMemberPermission `json:"permissions"` + AdminPermissions dbModel.RoomAdminPermission `json:"adminPermissions"` +} + +type RoomApproveMemberReq = UserIDReq +type RoomBanMemberReq = UserIDReq +type RoomUnbanMemberReq = UserIDReq + +type RoomSetMemberPermissionsReq struct { + UserIDReq + Permissions dbModel.RoomMemberPermission `json:"permissions"` +} + +type RoomMeResp struct { + UserID string `json:"userId"` + RoomID string `json:"roomId"` + JoinAt int64 `json:"joinAt"` + Role dbModel.RoomMemberRole `json:"role"` + Permissions dbModel.RoomMemberPermission `json:"permissions"` + AdminPermissions dbModel.RoomAdminPermission `json:"adminPermissions"` +} + +type RoomSetAdminReq struct { + UserIDReq + AdminPermissions dbModel.RoomAdminPermission `json:"adminPermissions"` +} + +type RoomSetMemberReq struct { + UserIDReq + Permissions dbModel.RoomMemberPermission `json:"permissions"` +} + +type RoomSetAdminPermissionsReq struct { + UserIDReq + AdminPermissions dbModel.RoomAdminPermission `json:"adminPermissions"` +} diff --git a/server/model/room.go b/server/model/room.go index 3470695..c897362 100644 --- a/server/model/room.go +++ b/server/model/room.go @@ -8,7 +8,6 @@ import ( "github.com/gin-gonic/gin" "github.com/synctv-org/synctv/internal/model" - dbModel "github.com/synctv-org/synctv/internal/model" ) var ( @@ -27,9 +26,9 @@ func (f FormatEmptyPasswordError) Error() string { } type CreateRoomReq struct { - RoomName string `json:"roomName"` - Password string `json:"password"` - Setting dbModel.RoomSettings `json:"setting"` + RoomName string `json:"roomName"` + Password string `json:"password"` + Hidden bool `json:"hidden"` } func (c *CreateRoomReq) Decode(ctx *gin.Context) error { @@ -121,7 +120,7 @@ func (r *RoomIDReq) Validate() error { return nil } -type SetRoomSettingReq dbModel.RoomSettings +type SetRoomSettingReq map[string]any func (s *SetRoomSettingReq) Decode(ctx *gin.Context) error { return json.NewDecoder(ctx.Request.Body).Decode(s) @@ -130,13 +129,3 @@ func (s *SetRoomSettingReq) Decode(ctx *gin.Context) error { func (s *SetRoomSettingReq) Validate() error { return nil } - -type RoomUsersResp struct { - UserID string `json:"userId"` - Username string `json:"username"` - Role dbModel.Role `json:"role"` - JoinAt int64 `json:"joinAt"` - RoomID string `json:"roomId"` - Status dbModel.RoomUserStatus `json:"status"` - Permissions dbModel.RoomUserPermission `json:"permissions"` -}