live-sfu-demo/internal/server/manage.go

486 lines
15 KiB
Go

package server
import (
"encoding/json"
"net/http"
"strings"
"time"
"sync-live/internal/auth"
"sync-live/internal/room"
)
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 != "" {
newRoom.PasswordHash = body.Password
}
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 != "" {
created.PasswordHash = body.Password
}
s.hub.createRoom(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 {
rm.PasswordHash = *body.Password
}
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 {
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 err := s.roomSvc.UpdateRoomSettings(r.Context(), roomName, actorName(r), &newSettings); err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
json.NewEncoder(w).Encode(map[string]interface{}{"ok": true})
}
func actorName(r *http.Request) string {
u, _ := auth.FromContext(r.Context())
if u != nil {
return u.Username
}
return ""
}
func stringOr(cur string, override *string) string {
if override != nil {
return *override
}
return cur
}