548 lines
17 KiB
Go
548 lines
17 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 != "" {
|
|
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
|
|
}
|