747 lines
24 KiB
Go
747 lines
24 KiB
Go
package server
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"sync-live/internal/auth"
|
|
"sync-live/internal/room"
|
|
)
|
|
|
|
func (s *Server) isGlobalAdmin(r *http.Request) bool {
|
|
u, _ := auth.FromContext(r.Context())
|
|
if u == nil {
|
|
return false
|
|
}
|
|
return u.Role == auth.RoleRoot || u.Role == auth.RoleAdmin
|
|
}
|
|
|
|
func (s *Server) handleManageRooms(w http.ResponseWriter, r *http.Request) {
|
|
if s.roomSvc == nil || s.roomSvc.Store().DB() == nil {
|
|
http.Error(w, "room service not configured", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
rooms, err := s.roomSvc.Store().ListRooms(r.Context())
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
enhanced := make([]map[string]interface{}, 0, len(rooms))
|
|
for _, rm := range rooms {
|
|
targets := s.hub.targets(rm.Name)
|
|
members, _ := s.roomSvc.Store().ListMembers(r.Context(), rm.Name)
|
|
enhanced = append(enhanced, map[string]interface{}{
|
|
"room": rm,
|
|
"targets": targets,
|
|
"member_count": len(members),
|
|
"live": len(targets) > 0,
|
|
})
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{"rooms": enhanced})
|
|
case http.MethodPost:
|
|
var body struct {
|
|
Name string `json:"name"`
|
|
DisplayName string `json:"display_name"`
|
|
Description string `json:"description"`
|
|
IsPublic *bool `json:"is_public"`
|
|
RequireApproval *bool `json:"require_approval"`
|
|
MaxMembers *int `json:"max_members"`
|
|
Password string `json:"password"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
http.Error(w, "bad json: "+err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
name := strings.TrimSpace(body.Name)
|
|
if name == "" {
|
|
http.Error(w, "room name required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
u, _ := auth.FromContext(r.Context())
|
|
creator := ""
|
|
if u != nil {
|
|
creator = u.Username
|
|
}
|
|
if creator == "" {
|
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
settings := room.DefaultSettings()
|
|
if body.IsPublic != nil {
|
|
settings.IsPublic = *body.IsPublic
|
|
}
|
|
if body.RequireApproval != nil {
|
|
settings.RequireApproval = *body.RequireApproval
|
|
}
|
|
if body.MaxMembers != nil && *body.MaxMembers > 0 {
|
|
settings.MaxMembers = *body.MaxMembers
|
|
}
|
|
newRoom := room.NewRoom(name, creator)
|
|
newRoom.DisplayName = body.DisplayName
|
|
if newRoom.DisplayName == "" {
|
|
newRoom.DisplayName = name
|
|
}
|
|
newRoom.Description = body.Description
|
|
newRoom.Settings = settings
|
|
newRoom.IsPublic = settings.IsPublic
|
|
newRoom.RequireApproval = settings.RequireApproval
|
|
newRoom.MaxMembers = settings.MaxMembers
|
|
if body.Password != "" {
|
|
hash, err := room.HashRoomPassword(body.Password)
|
|
if err != nil {
|
|
http.Error(w, "hash password: "+err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
newRoom.PasswordHash = hash
|
|
}
|
|
created, err := s.roomSvc.CreateRoom(r.Context(), name, creator, settings)
|
|
if err != nil {
|
|
http.Error(w, "create room: "+err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
created.Description = body.Description
|
|
created.DisplayName = newRoom.DisplayName
|
|
if body.Password != "" {
|
|
hash, _ := room.HashRoomPassword(body.Password)
|
|
created.PasswordHash = hash
|
|
}
|
|
s.hub.createRoom(name)
|
|
s.hub.broadcastRooms("create", name)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "room": created})
|
|
default:
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
}
|
|
}
|
|
|
|
func (s *Server) handleManageRoomDetail(w http.ResponseWriter, r *http.Request) {
|
|
if s.roomSvc == nil {
|
|
http.Error(w, "room service not configured", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
roomName := r.PathValue("room")
|
|
if roomName == "" {
|
|
roomName = r.URL.Query().Get("room")
|
|
}
|
|
if roomName == "" {
|
|
http.Error(w, "room required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
rm, err := s.roomSvc.Store().GetRoom(r.Context(), roomName)
|
|
if err != nil {
|
|
http.Error(w, "room not found: "+err.Error(), http.StatusNotFound)
|
|
return
|
|
}
|
|
members, _ := s.roomSvc.Store().ListMembers(r.Context(), roomName)
|
|
targets := s.hub.targets(roomName)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"room": rm,
|
|
"members": members,
|
|
"targets": targets,
|
|
})
|
|
case http.MethodPatch, http.MethodPut:
|
|
var body struct {
|
|
DisplayName *string `json:"display_name"`
|
|
Description *string `json:"description"`
|
|
Status *string `json:"status"`
|
|
IsPublic *bool `json:"is_public"`
|
|
RequireApproval *bool `json:"require_approval"`
|
|
MaxMembers *int `json:"max_members"`
|
|
Password *string `json:"password"`
|
|
ChatEnabled *bool `json:"chat_enabled"`
|
|
AllowGuestJoin *bool `json:"allow_guest_join"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
http.Error(w, "bad json: "+err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
u, _ := auth.FromContext(r.Context())
|
|
actor := ""
|
|
if u != nil {
|
|
actor = u.Username
|
|
}
|
|
rm, err := s.roomSvc.Store().GetRoom(r.Context(), roomName)
|
|
if err != nil {
|
|
http.Error(w, "room not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
if err := s.roomSvc.EnsurePermission(r.Context(), roomName, actor, room.PermManageRoom); err != nil {
|
|
http.Error(w, "forbidden: "+err.Error(), http.StatusForbidden)
|
|
return
|
|
}
|
|
settings := rm.Settings
|
|
if settings == nil {
|
|
settings = room.DefaultSettings()
|
|
}
|
|
if body.IsPublic != nil {
|
|
settings.IsPublic = *body.IsPublic
|
|
}
|
|
if body.RequireApproval != nil {
|
|
settings.RequireApproval = *body.RequireApproval
|
|
}
|
|
if body.MaxMembers != nil {
|
|
settings.MaxMembers = *body.MaxMembers
|
|
}
|
|
if body.ChatEnabled != nil {
|
|
settings.ChatEnabled = *body.ChatEnabled
|
|
}
|
|
if body.AllowGuestJoin != nil {
|
|
settings.AllowGuestJoin = *body.AllowGuestJoin
|
|
}
|
|
if body.Status != nil {
|
|
status := room.RoomStatus(*body.Status)
|
|
_ = s.roomSvc.Store().UpdateRoomStatus(r.Context(), roomName, status)
|
|
}
|
|
if body.DisplayName != nil || body.Description != nil || body.Password != nil || body.IsPublic != nil {
|
|
rm.DisplayName = stringOr(rm.DisplayName, body.DisplayName)
|
|
rm.Description = stringOr(rm.Description, body.Description)
|
|
rm.IsPublic = settings.IsPublic
|
|
rm.RequireApproval = settings.RequireApproval
|
|
rm.MaxMembers = settings.MaxMembers
|
|
if body.Password != nil {
|
|
hash, err := room.HashRoomPassword(*body.Password)
|
|
if err != nil {
|
|
http.Error(w, "hash password: "+err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
rm.PasswordHash = hash
|
|
}
|
|
rm.Settings = settings
|
|
rm.UpdatedAt = time.Now()
|
|
_ = s.roomSvc.Store().UpdateRoomSettings(r.Context(), roomName, settings)
|
|
} else {
|
|
_ = s.roomSvc.Store().UpdateRoomSettings(r.Context(), roomName, settings)
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{"ok": true})
|
|
case http.MethodDelete:
|
|
u, _ := auth.FromContext(r.Context())
|
|
actor := ""
|
|
if u != nil {
|
|
actor = u.Username
|
|
}
|
|
if err := s.roomSvc.EnsurePermission(r.Context(), roomName, actor, room.PermDeleteRoom); err != nil {
|
|
http.Error(w, "forbidden: "+err.Error(), http.StatusForbidden)
|
|
return
|
|
}
|
|
if err := s.roomSvc.Store().DeleteRoom(r.Context(), roomName); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
s.hub.removeRoom(roomName)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{"ok": true})
|
|
default:
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
}
|
|
}
|
|
|
|
func (s *Server) handleManageMembers(w http.ResponseWriter, r *http.Request) {
|
|
roomName := r.PathValue("room")
|
|
if roomName == "" {
|
|
http.Error(w, "room required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
members, err := s.roomSvc.Store().ListMembers(r.Context(), roomName)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
rm, _ := s.roomSvc.Store().GetRoom(r.Context(), roomName)
|
|
var settings *room.RoomSettings
|
|
if rm != nil {
|
|
settings = rm.Settings
|
|
}
|
|
out := make([]map[string]interface{}, 0, len(members))
|
|
for _, m := range members {
|
|
eff := m.EffectivePermissions(settings)
|
|
out = append(out, map[string]interface{}{
|
|
"username": m.Username,
|
|
"role": m.Role.String(),
|
|
"status": m.Status.String(),
|
|
"joined_at": m.JoinedAt.Unix(),
|
|
"effective_permissions": eff.Names(),
|
|
"effective_bits": eff.Bits(),
|
|
"added": m.AddedPerms,
|
|
"removed": m.RemovedPerms,
|
|
"admin_added": m.AdminAdded,
|
|
"admin_removed": m.AdminRemoved,
|
|
"version": m.Version,
|
|
})
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{"members": out, "room": roomName})
|
|
case http.MethodPost:
|
|
var body struct {
|
|
Username string `json:"username"`
|
|
Role string `json:"role"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
http.Error(w, "bad json: "+err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
actorID := actorName(r)
|
|
role, err := room.ParseRoomRole(body.Role)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
if err := s.roomSvc.UpdateMemberRole(r.Context(), roomName, actorID, body.Username, role); err != nil {
|
|
http.Error(w, err.Error(), http.StatusForbidden)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{"ok": true})
|
|
default:
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
}
|
|
}
|
|
|
|
func (s *Server) handleManageMemberRole(w http.ResponseWriter, r *http.Request) {
|
|
roomName := r.PathValue("room")
|
|
target := r.PathValue("user")
|
|
if roomName == "" || target == "" {
|
|
http.Error(w, "room and user required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
switch r.Method {
|
|
case http.MethodPut, http.MethodPost:
|
|
var body struct {
|
|
Role string `json:"role"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
http.Error(w, "bad json", http.StatusBadRequest)
|
|
return
|
|
}
|
|
role, err := room.ParseRoomRole(body.Role)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
if err := s.roomSvc.UpdateMemberRole(r.Context(), roomName, actorName(r), target, role); err != nil {
|
|
http.Error(w, err.Error(), http.StatusForbidden)
|
|
return
|
|
}
|
|
json.NewEncoder(w).Encode(map[string]interface{}{"ok": true})
|
|
case http.MethodDelete:
|
|
if err := s.roomSvc.KickMember(r.Context(), roomName, actorName(r), target); err != nil {
|
|
http.Error(w, err.Error(), http.StatusForbidden)
|
|
return
|
|
}
|
|
json.NewEncoder(w).Encode(map[string]interface{}{"ok": true})
|
|
default:
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
}
|
|
}
|
|
|
|
func (s *Server) handleManageMemberPerms(w http.ResponseWriter, r *http.Request) {
|
|
roomName := r.PathValue("room")
|
|
target := r.PathValue("user")
|
|
if roomName == "" || target == "" {
|
|
http.Error(w, "room and user required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
var body struct {
|
|
Added uint64 `json:"added"`
|
|
Removed uint64 `json:"removed"`
|
|
IsAdmin bool `json:"is_admin"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
http.Error(w, "bad json", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if err := s.roomSvc.UpdateMemberPermissions(r.Context(), roomName, actorName(r), target, body.Added, body.Removed, body.IsAdmin); err != nil {
|
|
http.Error(w, err.Error(), http.StatusForbidden)
|
|
return
|
|
}
|
|
json.NewEncoder(w).Encode(map[string]interface{}{"ok": true})
|
|
}
|
|
|
|
func (s *Server) handleManagePermissionsMatrix(w http.ResponseWriter, r *http.Request) {
|
|
roomName := r.PathValue("room")
|
|
if r.Method == http.MethodGet && roomName == "" {
|
|
defs := map[string]interface{}{
|
|
"roles": map[string]interface{}{
|
|
"creator": map[string]interface{}{"bits": room.PermDefaultCreator.Bits(), "perms": room.PermDefaultCreator.Names()},
|
|
"admin": map[string]interface{}{"bits": room.PermDefaultAdmin.Bits(), "perms": room.PermDefaultAdmin.Names()},
|
|
"member": map[string]interface{}{"bits": room.PermDefaultMember.Bits(), "perms": room.PermDefaultMember.Names()},
|
|
"guest": map[string]interface{}{"bits": room.PermDefaultGuest.Bits(), "perms": room.PermDefaultGuest.Names()},
|
|
},
|
|
"all_permissions": room.PermAll.Names(),
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(defs)
|
|
return
|
|
}
|
|
if roomName == "" {
|
|
http.Error(w, "room required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
members, err := s.roomSvc.Store().ListMembers(r.Context(), roomName)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
rm, _ := s.roomSvc.Store().GetRoom(r.Context(), roomName)
|
|
settings := room.DefaultSettings()
|
|
if rm != nil && rm.Settings != nil {
|
|
settings = rm.Settings
|
|
}
|
|
matrix := make([]map[string]interface{}, 0, len(members))
|
|
for _, m := range members {
|
|
matrix = append(matrix, map[string]interface{}{
|
|
"username": m.Username,
|
|
"role": m.Role.String(),
|
|
"base": room.DefaultForRole(m.Role).Names(),
|
|
"effective": m.EffectivePermissions(settings).Names(),
|
|
"added": m.AddedPerms,
|
|
"removed": m.RemovedPerms,
|
|
})
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{"room": roomName, "settings": settings, "matrix": matrix})
|
|
}
|
|
|
|
func (s *Server) handleManageOverview(w http.ResponseWriter, r *http.Request) {
|
|
rooms, _ := s.roomSvc.Store().ListRooms(r.Context())
|
|
totalRooms := len(rooms)
|
|
liveRooms := 0
|
|
totalMembers := 0
|
|
for _, rm := range rooms {
|
|
if len(s.hub.targets(rm.Name)) > 0 {
|
|
liveRooms++
|
|
}
|
|
cnt, _ := s.roomSvc.Store().CountMembers(r.Context(), rm.Name)
|
|
totalMembers += cnt
|
|
}
|
|
users := 0
|
|
if s.auth != nil && s.auth.Store != nil {
|
|
users = len(s.auth.Store.List())
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"rooms_total": totalRooms,
|
|
"rooms_live": liveRooms,
|
|
"members_total": totalMembers,
|
|
"users_total": users,
|
|
"backends": s.svc.Backends(),
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleManageJoin(w http.ResponseWriter, r *http.Request) {
|
|
roomName := r.PathValue("room")
|
|
var body struct {
|
|
Password string `json:"password"`
|
|
}
|
|
_ = json.NewDecoder(r.Body).Decode(&body)
|
|
user := actorName(r)
|
|
if user == "" {
|
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
m, err := s.roomSvc.JoinRoom(r.Context(), roomName, user, body.Password)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "member": m})
|
|
}
|
|
|
|
func (s *Server) handleManageLeave(w http.ResponseWriter, r *http.Request) {
|
|
roomName := r.PathValue("room")
|
|
user := actorName(r)
|
|
if err := s.roomSvc.LeaveRoom(r.Context(), roomName, user); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
json.NewEncoder(w).Encode(map[string]interface{}{"ok": true})
|
|
}
|
|
|
|
func (s *Server) handleManageRoomSettings(w http.ResponseWriter, r *http.Request) {
|
|
roomName := r.PathValue("room")
|
|
rm, err := s.roomSvc.Store().GetRoom(r.Context(), roomName)
|
|
if err != nil {
|
|
http.Error(w, "room not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
if r.Method == http.MethodGet {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(rm.Settings)
|
|
return
|
|
}
|
|
var newSettings room.RoomSettings
|
|
if err := json.NewDecoder(r.Body).Decode(&newSettings); err != nil {
|
|
http.Error(w, "bad json", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if newSettings.Distributors != nil {
|
|
if err := room.ValidateDistributorsList(room.ParseDistributors(*newSettings.Distributors)); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
norm := room.NormalizeDistributorsString(*newSettings.Distributors)
|
|
newSettings.Distributors = &norm
|
|
}
|
|
if !s.isGlobalAdmin(r) {
|
|
if err := s.roomSvc.UpdateRoomSettings(r.Context(), roomName, actorName(r), &newSettings); err != nil {
|
|
http.Error(w, err.Error(), http.StatusForbidden)
|
|
return
|
|
}
|
|
} else {
|
|
if err := s.roomSvc.Store().UpdateRoomSettings(r.Context(), roomName, &newSettings); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "settings": newSettings})
|
|
}
|
|
|
|
func actorName(r *http.Request) string {
|
|
u, _ := auth.FromContext(r.Context())
|
|
if u != nil {
|
|
return u.Username
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (s *Server) handleManageMemberApprove(w http.ResponseWriter, r *http.Request) {
|
|
roomName := r.PathValue("room")
|
|
target := r.PathValue("user")
|
|
if roomName == "" || target == "" {
|
|
http.Error(w, "room and user required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if err := s.roomSvc.ApproveJoin(r.Context(), roomName, actorName(r), target); err != nil {
|
|
http.Error(w, err.Error(), http.StatusForbidden)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{"ok": true})
|
|
}
|
|
|
|
func (s *Server) handleManageMemberReject(w http.ResponseWriter, r *http.Request) {
|
|
roomName := r.PathValue("room")
|
|
target := r.PathValue("user")
|
|
if roomName == "" || target == "" {
|
|
http.Error(w, "room and user required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if err := s.roomSvc.RejectJoin(r.Context(), roomName, actorName(r), target); err != nil {
|
|
http.Error(w, err.Error(), http.StatusForbidden)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{"ok": true})
|
|
}
|
|
|
|
func (s *Server) handleManageTransferOwnership(w http.ResponseWriter, r *http.Request) {
|
|
roomName := r.PathValue("room")
|
|
if roomName == "" {
|
|
http.Error(w, "room required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
var body struct {
|
|
TargetUser string `json:"target_user"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil || body.TargetUser == "" {
|
|
http.Error(w, "target_user required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if err := s.roomSvc.TransferOwnership(r.Context(), roomName, actorName(r), body.TargetUser); err != nil {
|
|
http.Error(w, err.Error(), http.StatusForbidden)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{"ok": true})
|
|
}
|
|
|
|
func (s *Server) handleManageRoomDistribution(w http.ResponseWriter, r *http.Request) {
|
|
roomName := r.PathValue("room")
|
|
if roomName == "" {
|
|
http.Error(w, "room required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
rm, err := s.roomSvc.Store().GetRoom(r.Context(), roomName)
|
|
if err != nil {
|
|
http.Error(w, "room not found: "+err.Error(), http.StatusNotFound)
|
|
return
|
|
}
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
globalList := s.cfg.DistributorList()
|
|
if rm.Settings == nil {
|
|
rm.Settings = room.DefaultSettings()
|
|
}
|
|
effective := rm.Settings.EffectiveDistributors(globalList)
|
|
hasCustom := rm.Settings.HasCustomDistributors()
|
|
var customList []string
|
|
var customRaw *string
|
|
if hasCustom {
|
|
customRaw = rm.Settings.Distributors
|
|
customList = room.ParseDistributors(*customRaw)
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]any{
|
|
"room": roomName,
|
|
"global": globalList,
|
|
"custom": customRaw,
|
|
"custom_list": customList,
|
|
"effective": effective,
|
|
"has_custom": hasCustom,
|
|
})
|
|
case http.MethodPut, http.MethodPost, http.MethodPatch:
|
|
var body struct {
|
|
Distributors *string `json:"distributors"`
|
|
List []string `json:"list"`
|
|
}
|
|
rawBody := make([]byte, 0, 4096)
|
|
buf := make([]byte, 4096)
|
|
for {
|
|
n, err := r.Body.Read(buf)
|
|
if n > 0 {
|
|
rawBody = append(rawBody, buf[:n]...)
|
|
}
|
|
if err != nil {
|
|
break
|
|
}
|
|
}
|
|
if len(rawBody) > 0 {
|
|
_ = json.Unmarshal(rawBody, &body)
|
|
if body.Distributors == nil && body.List == nil {
|
|
var flat map[string]any
|
|
if err := json.Unmarshal(rawBody, &flat); err == nil {
|
|
if v, ok := flat["distributors"]; ok {
|
|
if sv, ok := v.(string); ok {
|
|
body.Distributors = &sv
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
var toSet *string
|
|
if body.List != nil {
|
|
joined := strings.Join(body.List, ",")
|
|
if err := room.ValidateDistributorsList(room.ParseDistributors(joined)); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
norm := room.NormalizeDistributorsString(joined)
|
|
toSet = &norm
|
|
} else if body.Distributors != nil {
|
|
raw := strings.TrimSpace(*body.Distributors)
|
|
if raw == "" {
|
|
empty := ""
|
|
toSet = &empty
|
|
} else {
|
|
if err := room.ValidateDistributorsList(room.ParseDistributors(raw)); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
for _, tok := range strings.Split(raw, ",") {
|
|
tok = strings.TrimSpace(strings.ToLower(tok))
|
|
if tok == "" {
|
|
continue
|
|
}
|
|
if tok == "srs" || tok == "hls" {
|
|
tok = "srs-hls"
|
|
}
|
|
if tok == "cloudflare" || tok == "cf-sfu" {
|
|
tok = "cf"
|
|
}
|
|
if tok != "srs-hls" && tok != "cf" && tok != "cdn" {
|
|
http.Error(w, "unsupported distributor "+tok, http.StatusBadRequest)
|
|
return
|
|
}
|
|
}
|
|
norm := room.NormalizeDistributorsString(raw)
|
|
toSet = &norm
|
|
}
|
|
} else {
|
|
http.Error(w, "distributors or list required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if !s.isGlobalAdmin(r) {
|
|
if err := s.roomSvc.EnsurePermission(r.Context(), roomName, actorName(r), room.PermManageRoom); err != nil {
|
|
http.Error(w, "forbidden: "+err.Error(), http.StatusForbidden)
|
|
return
|
|
}
|
|
}
|
|
newSettings := rm.Settings
|
|
if newSettings == nil {
|
|
newSettings = room.DefaultSettings()
|
|
}
|
|
newSettings.Distributors = toSet
|
|
if err := s.roomSvc.Store().UpdateRoomSettings(r.Context(), roomName, newSettings); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]any{"ok": true, "distributors": *toSet, "effective": room.ParseDistributors(*toSet)})
|
|
case http.MethodDelete:
|
|
if !s.isGlobalAdmin(r) {
|
|
if err := s.roomSvc.EnsurePermission(r.Context(), roomName, actorName(r), room.PermManageRoom); err != nil {
|
|
http.Error(w, "forbidden: "+err.Error(), http.StatusForbidden)
|
|
return
|
|
}
|
|
}
|
|
newSettings := rm.Settings
|
|
if newSettings == nil {
|
|
newSettings = room.DefaultSettings()
|
|
}
|
|
newSettings.Distributors = nil
|
|
if err := s.roomSvc.Store().UpdateRoomSettings(r.Context(), roomName, newSettings); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]any{"ok": true, "cleared": true, "effective": s.cfg.DistributorList()})
|
|
default:
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
}
|
|
}
|
|
|
|
func stringOr(cur string, override *string) string {
|
|
if override != nil {
|
|
return *override
|
|
}
|
|
return cur
|
|
}
|
|
|
|
func (s *Server) handleManageMemberBan(w http.ResponseWriter, r *http.Request) {
|
|
roomName := r.PathValue("room")
|
|
target := r.PathValue("user")
|
|
if roomName == "" || target == "" {
|
|
http.Error(w, "room and user required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if !s.isGlobalAdmin(r) {
|
|
if err := s.roomSvc.BanMember(r.Context(), roomName, actorName(r), target); err != nil {
|
|
http.Error(w, err.Error(), http.StatusForbidden)
|
|
return
|
|
}
|
|
json.NewEncoder(w).Encode(map[string]interface{}{"ok": true})
|
|
return
|
|
}
|
|
m, err := s.roomSvc.Store().GetMember(r.Context(), roomName, target)
|
|
if err != nil {
|
|
m = room.NewMember(roomName, target, room.RoleGuest)
|
|
}
|
|
m.Status = room.MemberBanned
|
|
if err := s.roomSvc.Store().UpsertMember(r.Context(), m); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
json.NewEncoder(w).Encode(map[string]interface{}{"ok": true})
|
|
}
|