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 != "" { 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) 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 { 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 (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 stringOr(cur string, override *string) string { if override != nil { return *override } return cur }