package servergroup import ( "database/sql" "encoding/json" "net/http" "git.dustin.coffee/hobokenchicken/dumpsterChat/internal/middleware" "github.com/go-chi/chi/v5" ) type Handler struct { db *sql.DB } func NewHandler(db *sql.DB) *Handler { return &Handler{db: db} } func (h *Handler) RegisterRoutes(r chi.Router) { r.Post("/", h.Create) r.Get("/", h.List) r.Get("/{groupID}", h.Get) r.Patch("/{groupID}", h.Update) r.Delete("/{groupID}", h.Delete) } type groupResponse struct { ID string `json:"id"` ServerID string `json:"server_id"` Name string `json:"name"` Position int `json:"position"` CreatedAt string `json:"created_at"` } type createGroupRequest struct { Name string `json:"name"` Position *int `json:"position"` } type updateGroupRequest struct { Name *string `json:"name"` Position *int `json:"position"` } // @Summary Create a server group // @Description Create a new channel group within a server // @Tags server_groups // @Accept json // @Produce json // @Security SessionAuth // @Param serverID path string true "Server ID" // @Param body body createGroupRequest true "Group data" // @Success 201 {object} groupResponse // @Failure 400 {object} map[string]string // @Router /servers/{serverID}/groups [post] func (h *Handler) Create(w http.ResponseWriter, r *http.Request) { userID, ok := middleware.UserIDFromContext(r.Context()) if !ok { http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized) return } serverID := chi.URLParam(r, "serverID") var req createGroupRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest) return } if req.Name == "" { http.Error(w, `{"error":"name is required"}`, http.StatusBadRequest) return } // Verify ownership or MANAGE_CHANNELS var ownerID string err := h.db.QueryRowContext(r.Context(), `SELECT owner_id FROM servers WHERE id = $1`, serverID).Scan(&ownerID) if err != nil { http.Error(w, `{"error":"server not found"}`, http.StatusNotFound) return } if ownerID != userID { // Check permission var hasPerm bool err = h.db.QueryRowContext(r.Context(), ` SELECT EXISTS ( SELECT 1 FROM role_permissions rp JOIN member_roles mr ON mr.role_id = rp.role_id WHERE mr.user_id = $1 AND mr.server_id = $2 AND rp.permission = 'manage_channels' ) `, userID, serverID).Scan(&hasPerm) if err != nil || !hasPerm { http.Error(w, `{"error":"forbidden"}`, http.StatusForbidden) return } } var position int if req.Position == nil { // Auto-assign position (max + 1) err = h.db.QueryRowContext(r.Context(), `SELECT COALESCE(MAX(position), -1) + 1 FROM server_groups WHERE server_id = $1`, serverID).Scan(&position) if err != nil { http.Error(w, `{"error":"server error"}`, http.StatusInternalServerError) return } } else { position = *req.Position } var grp groupResponse err = h.db.QueryRowContext(r.Context(), ` INSERT INTO server_groups (server_id, name, position) VALUES ($1, $2, $3) RETURNING id, server_id, name, position, created_at `, serverID, req.Name, position).Scan(&grp.ID, &grp.ServerID, &grp.Name, &grp.Position, &grp.CreatedAt) if err != nil { http.Error(w, `{"error":"failed to create group (name may already exist)"}`, http.StatusConflict) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(grp) } // @Summary List server groups // @Description List all channel groups for a server, ordered by position // @Tags server_groups // @Produce json // @Security SessionAuth // @Param serverID path string true "Server ID" // @Success 200 {array} groupResponse // @Router /servers/{serverID}/groups [get] func (h *Handler) List(w http.ResponseWriter, r *http.Request) { serverID := chi.URLParam(r, "serverID") rows, err := h.db.QueryContext(r.Context(), ` SELECT id, server_id, name, position, created_at FROM server_groups WHERE server_id = $1 ORDER BY position, name `, serverID) if err != nil { http.Error(w, `{"error":"server error"}`, http.StatusInternalServerError) return } defer rows.Close() groups := make([]groupResponse, 0) for rows.Next() { var grp groupResponse if err := rows.Scan(&grp.ID, &grp.ServerID, &grp.Name, &grp.Position, &grp.CreatedAt); err != nil { http.Error(w, `{"error":"server error"}`, http.StatusInternalServerError) return } groups = append(groups, grp) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(groups) } // @Summary Get a server group // @Tags server_groups // @Produce json // @Security SessionAuth // @Param serverID path string true "Server ID" // @Param groupID path string true "Group ID" // @Success 200 {object} groupResponse // @Router /servers/{serverID}/groups/{groupID} [get] func (h *Handler) Get(w http.ResponseWriter, r *http.Request) { groupID := chi.URLParam(r, "groupID") var grp groupResponse err := h.db.QueryRowContext(r.Context(), ` SELECT id, server_id, name, position, created_at FROM server_groups WHERE id = $1 `, groupID).Scan(&grp.ID, &grp.ServerID, &grp.Name, &grp.Position, &grp.CreatedAt) if err != nil { http.Error(w, `{"error":"group not found"}`, http.StatusNotFound) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(grp) } // @Summary Update a server group // @Tags server_groups // @Accept json // @Produce json // @Security SessionAuth // @Param serverID path string true "Server ID" // @Param groupID path string true "Group ID" // @Param body body updateGroupRequest true "Group update data" // @Success 200 {object} groupResponse // @Router /servers/{serverID}/groups/{groupID} [patch] func (h *Handler) Update(w http.ResponseWriter, r *http.Request) { userID, ok := middleware.UserIDFromContext(r.Context()) if !ok { http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized) return } serverID := chi.URLParam(r, "serverID") groupID := chi.URLParam(r, "groupID") var req updateGroupRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest) return } if req.Name == nil && req.Position == nil { http.Error(w, `{"error":"nothing to update"}`, http.StatusBadRequest) return } var ownerID string err := h.db.QueryRowContext(r.Context(), `SELECT owner_id FROM servers WHERE id = $1`, serverID).Scan(&ownerID) if err != nil { http.Error(w, `{"error":"server not found"}`, http.StatusNotFound) return } if ownerID != userID { var hasPerm bool err = h.db.QueryRowContext(r.Context(), ` SELECT EXISTS ( SELECT 1 FROM role_permissions rp JOIN member_roles mr ON mr.role_id = rp.role_id WHERE mr.user_id = $1 AND mr.server_id = $2 AND rp.permission = 'manage_channels' ) `, userID, serverID).Scan(&hasPerm) if err != nil || !hasPerm { http.Error(w, `{"error":"forbidden"}`, http.StatusForbidden) return } } var grp groupResponse err = h.db.QueryRowContext(r.Context(), ` UPDATE server_groups SET name = COALESCE($1, name), position = COALESCE($2, position) WHERE id = $3 AND server_id = $4 RETURNING id, server_id, name, position, created_at `, req.Name, req.Position, groupID, serverID).Scan(&grp.ID, &grp.ServerID, &grp.Name, &grp.Position, &grp.CreatedAt) if err != nil { http.Error(w, `{"error":"group not found"}`, http.StatusNotFound) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(grp) } // @Summary Delete a server group // @Tags server_groups // @Security SessionAuth // @Param serverID path string true "Server ID" // @Param groupID path string true "Group ID" // @Success 204 // @Router /servers/{serverID}/groups/{groupID} [delete] func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) { userID, ok := middleware.UserIDFromContext(r.Context()) if !ok { http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized) return } serverID := chi.URLParam(r, "serverID") groupID := chi.URLParam(r, "groupID") // Only owner or manage_channels can delete groups var ownerID string err := h.db.QueryRowContext(r.Context(), `SELECT owner_id FROM servers WHERE id = $1`, serverID).Scan(&ownerID) if err != nil { http.Error(w, `{"error":"server not found"}`, http.StatusNotFound) return } if ownerID != userID { var hasPerm bool err = h.db.QueryRowContext(r.Context(), ` SELECT EXISTS ( SELECT 1 FROM role_permissions rp JOIN member_roles mr ON mr.role_id = rp.role_id WHERE mr.user_id = $1 AND mr.server_id = $2 AND rp.permission = 'manage_channels' ) `, userID, serverID).Scan(&hasPerm) if err != nil || !hasPerm { http.Error(w, `{"error":"forbidden"}`, http.StatusForbidden) return } } // Channels in this group get group_id set to NULL (via ON DELETE SET NULL) result, err := h.db.ExecContext(r.Context(), `DELETE FROM server_groups WHERE id = $1 AND server_id = $2`, groupID, serverID) if err != nil { http.Error(w, `{"error":"failed to delete group"}`, http.StatusInternalServerError) return } rows, _ := result.RowsAffected() if rows == 0 { http.Error(w, `{"error":"group not found"}`, http.StatusNotFound) return } w.WriteHeader(http.StatusNoContent) }