package handlers import ( "encoding/json" "net/http" "github.com/gorilla/mux" "go.mongodb.org/mongo-driver/v2/bson" "gitea.hostxtra.co.uk/mrhid6/notely/backend/internal/application/dto" "gitea.hostxtra.co.uk/mrhid6/notely/backend/internal/application/services" "gitea.hostxtra.co.uk/mrhid6/notely/backend/internal/interfaces/middleware" ) // CategoryHandler handles category endpoints type CategoryHandler struct { categoryService *services.CategoryService } // NewCategoryHandler creates a new category handler func NewCategoryHandler(categoryService *services.CategoryService) *CategoryHandler { return &CategoryHandler{categoryService: categoryService} } // GetCategoryTree returns the full category tree for a space func (h *CategoryHandler) GetCategoryTree(w http.ResponseWriter, r *http.Request) { userID, err := getUserObjectID(r) if err != nil { http.Error(w, "unauthorized", http.StatusUnauthorized) return } vars := mux.Vars(r) spaceID, err := bson.ObjectIDFromHex(vars["spaceId"]) if err != nil { http.Error(w, "invalid space id", http.StatusBadRequest) return } tree, err := h.categoryService.GetCategoryTree(r.Context(), spaceID, userID) if err != nil { http.Error(w, err.Error(), http.StatusForbidden) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(tree) } // CreateCategory creates a new category in a space func (h *CategoryHandler) CreateCategory(w http.ResponseWriter, r *http.Request) { userID, err := getUserObjectID(r) if err != nil { http.Error(w, "unauthorized", http.StatusUnauthorized) return } vars := mux.Vars(r) spaceID, err := bson.ObjectIDFromHex(vars["spaceId"]) if err != nil { http.Error(w, "invalid space id", http.StatusBadRequest) return } var req dto.CreateCategoryRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid request body", http.StatusBadRequest) return } category, err := h.categoryService.CreateCategory(r.Context(), spaceID, userID, &req) if err != nil { if err.Error() == "unauthorized" { http.Error(w, err.Error(), http.StatusForbidden) } else { http.Error(w, err.Error(), http.StatusBadRequest) } return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(category) } // UpdateCategory updates a category func (h *CategoryHandler) UpdateCategory(w http.ResponseWriter, r *http.Request) { userID, err := getUserObjectID(r) if err != nil { http.Error(w, "unauthorized", http.StatusUnauthorized) return } vars := mux.Vars(r) spaceID, err := bson.ObjectIDFromHex(vars["spaceId"]) if err != nil { http.Error(w, "invalid space id", http.StatusBadRequest) return } categoryID, err := bson.ObjectIDFromHex(vars["categoryId"]) if err != nil { http.Error(w, "invalid category id", http.StatusBadRequest) return } var req dto.UpdateCategoryRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid request body", http.StatusBadRequest) return } category, err := h.categoryService.UpdateCategory(r.Context(), categoryID, spaceID, userID, &req) if err != nil { if err.Error() == "unauthorized" { http.Error(w, err.Error(), http.StatusForbidden) } else { http.Error(w, err.Error(), http.StatusNotFound) } return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(category) } // DeleteCategory deletes a category func (h *CategoryHandler) DeleteCategory(w http.ResponseWriter, r *http.Request) { userID, err := getUserObjectID(r) if err != nil { http.Error(w, "unauthorized", http.StatusUnauthorized) return } vars := mux.Vars(r) spaceID, err := bson.ObjectIDFromHex(vars["spaceId"]) if err != nil { http.Error(w, "invalid space id", http.StatusBadRequest) return } categoryID, err := bson.ObjectIDFromHex(vars["categoryId"]) if err != nil { http.Error(w, "invalid category id", http.StatusBadRequest) return } var moveNotesTo *string if v := r.URL.Query().Get("moveNotesTo"); v != "" { moveNotesTo = &v } if err := h.categoryService.DeleteCategory(r.Context(), categoryID, spaceID, userID, moveNotesTo); err != nil { if err.Error() == "unauthorized" { http.Error(w, err.Error(), http.StatusForbidden) } else { http.Error(w, err.Error(), http.StatusNotFound) } return } w.WriteHeader(http.StatusNoContent) } // MoveCategory moves a category to a new parent func (h *CategoryHandler) MoveCategory(w http.ResponseWriter, r *http.Request) { userID, err := getUserObjectID(r) if err != nil { http.Error(w, "unauthorized", http.StatusUnauthorized) return } vars := mux.Vars(r) spaceID, err := bson.ObjectIDFromHex(vars["spaceId"]) if err != nil { http.Error(w, "invalid space id", http.StatusBadRequest) return } categoryID, err := bson.ObjectIDFromHex(vars["categoryId"]) if err != nil { http.Error(w, "invalid category id", http.StatusBadRequest) return } var body struct { ParentID *string `json:"parent_id"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { http.Error(w, "invalid request body", http.StatusBadRequest) return } category, err := h.categoryService.MoveCategory(r.Context(), categoryID, spaceID, userID, body.ParentID) if err != nil { if err.Error() == "unauthorized" { http.Error(w, err.Error(), http.StatusForbidden) } else { http.Error(w, err.Error(), http.StatusBadRequest) } return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(category) } // getUserObjectID extracts the user ObjectID from the request context func getUserObjectID(r *http.Request) (bson.ObjectID, error) { userIDStr, ok := r.Context().Value(middleware.UserIDKey).(string) if !ok || userIDStr == "" { return bson.NilObjectID, http.ErrNoCookie } return bson.ObjectIDFromHex(userIDStr) }