package api import ( "context" "database/sql" "encoding/json" "fmt" "net/http" "github.com/go-chi/chi/v5" "github.com/insertr/insertr/internal/auth" "github.com/insertr/insertr/internal/db" "github.com/insertr/insertr/internal/engine" "github.com/insertr/insertr/internal/sites" ) // ContentHandler handles all content-related HTTP requests type ContentHandler struct { repository db.ContentRepository authService *auth.AuthService siteManager *sites.SiteManager engine *engine.ContentEngine } // NewContentHandler creates a new content handler func NewContentHandler(database *db.Database, authService *auth.AuthService) *ContentHandler { // Create repository for all database operations repository := database.NewContentRepository() return &ContentHandler{ repository: repository, authService: authService, siteManager: nil, // Will be set via SetSiteManager engine: engine.NewContentEngine(repository), } } // SetSiteManager sets the site manager for file enhancement func (h *ContentHandler) SetSiteManager(siteManager *sites.SiteManager) { h.siteManager = siteManager } // GetContent handles GET /api/content/{id} func (h *ContentHandler) GetContent(w http.ResponseWriter, r *http.Request) { contentID := chi.URLParam(r, "id") siteID := r.URL.Query().Get("site_id") if siteID == "" { http.Error(w, "site_id parameter is required", http.StatusBadRequest) return } content, err := h.repository.GetContent(context.Background(), siteID, contentID) if err != nil { if err == sql.ErrNoRows { http.Error(w, "Content not found", http.StatusNotFound) return } http.Error(w, fmt.Sprintf("Database error: %v", err), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(content) } // GetAllContent handles GET /api/content with site_id parameter func (h *ContentHandler) GetAllContent(w http.ResponseWriter, r *http.Request) { siteID := r.URL.Query().Get("site_id") if siteID == "" { http.Error(w, "site_id parameter is required", http.StatusBadRequest) return } contentMap, err := h.repository.GetAllContent(context.Background(), siteID) if err != nil { http.Error(w, fmt.Sprintf("Database error: %v", err), http.StatusInternalServerError) return } // Convert map to slice for consistent API response var content []db.ContentItem for _, item := range contentMap { content = append(content, item) } response := db.ContentResponse{Content: content} w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } // GetBulkContent handles POST /api/content/bulk for batch fetching func (h *ContentHandler) GetBulkContent(w http.ResponseWriter, r *http.Request) { siteID := r.URL.Query().Get("site_id") contentIDs := r.URL.Query()["ids"] if siteID == "" { http.Error(w, "site_id parameter is required", http.StatusBadRequest) return } if len(contentIDs) == 0 { http.Error(w, "at least one content ID is required", http.StatusBadRequest) return } contentMap, err := h.repository.GetBulkContent(context.Background(), siteID, contentIDs) if err != nil { http.Error(w, fmt.Sprintf("Database error: %v", err), http.StatusInternalServerError) return } // Convert map to slice for consistent API response var content []db.ContentItem for _, item := range contentMap { content = append(content, item) } response := db.ContentResponse{Content: content} w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } // CreateOrUpdateContent handles POST /api/content for creating/updating content func (h *ContentHandler) CreateOrUpdateContent(w http.ResponseWriter, r *http.Request) { userInfo, authErr := h.authService.ExtractUserFromRequest(r) if authErr != nil { http.Error(w, "Authentication required", http.StatusUnauthorized) return } var req CreateContentRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, fmt.Sprintf("Invalid JSON: %v", err), http.StatusBadRequest) return } // Use engine to generate ID from HTML structure result, err := h.engine.ProcessContent(engine.ContentInput{ HTML: []byte(req.HTMLMarkup), FilePath: req.FilePath, SiteID: req.SiteID, Mode: engine.IDGeneration, }) if err != nil { http.Error(w, fmt.Sprintf("Failed to process content: %v", err), http.StatusInternalServerError) return } if len(result.Elements) == 0 { http.Error(w, "No content elements found in markup", http.StatusBadRequest) return } contentID := result.Elements[0].ID // Check if content already exists existingContent, _ := h.repository.GetContent(context.Background(), req.SiteID, contentID) var content *db.ContentItem if existingContent == nil { // Create new content content, err = h.repository.CreateContent( context.Background(), req.SiteID, contentID, req.HTMLContent, req.OriginalTemplate, userInfo.ID, ) } else { // Update existing content - this would need UpdateContent method in repository // For now, we'll return the existing content content = existingContent } if err != nil { http.Error(w, fmt.Sprintf("Failed to save content: %v", err), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(content) } // DeleteContent handles DELETE /api/content/{id} func (h *ContentHandler) DeleteContent(w http.ResponseWriter, r *http.Request) { _, authErr := h.authService.ExtractUserFromRequest(r) if authErr != nil { http.Error(w, "Authentication required", http.StatusUnauthorized) return } contentID := chi.URLParam(r, "id") siteID := r.URL.Query().Get("site_id") if siteID == "" { http.Error(w, "site_id parameter is required", http.StatusBadRequest) return } // Note: DeleteContent method would need to be added to repository interface // For now, return not implemented _ = contentID // Suppress unused variable warning http.Error(w, "Delete operation not yet implemented", http.StatusNotImplemented) } // UpdateContent handles PUT /api/content/{id} func (h *ContentHandler) UpdateContent(w http.ResponseWriter, r *http.Request) { contentID := chi.URLParam(r, "id") siteID := r.URL.Query().Get("site_id") if siteID == "" { http.Error(w, "site_id parameter is required", http.StatusBadRequest) return } var req struct { HTMLContent string `json:"html_content"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid JSON", http.StatusBadRequest) return } userInfo, authErr := h.authService.ExtractUserFromRequest(r) if authErr != nil { http.Error(w, "Authentication required", http.StatusUnauthorized) return } // Check if content exists existingContent, err := h.repository.GetContent(context.Background(), siteID, contentID) if err != nil { if err == sql.ErrNoRows { http.Error(w, "Content not found", http.StatusNotFound) return } http.Error(w, fmt.Sprintf("Database error: %v", err), http.StatusInternalServerError) return } if existingContent == nil { http.Error(w, "Content not found", http.StatusNotFound) return } // Update content using repository updatedContent, err := h.repository.UpdateContent(context.Background(), siteID, contentID, req.HTMLContent, userInfo.ID) if err != nil { http.Error(w, fmt.Sprintf("Failed to update content: %v", err), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(updatedContent) } // ReorderCollection handles PUT /api/collections/{id}/reorder func (h *ContentHandler) ReorderCollection(w http.ResponseWriter, r *http.Request) { collectionID := chi.URLParam(r, "id") siteID := r.URL.Query().Get("site_id") if siteID == "" { http.Error(w, "site_id parameter is required", http.StatusBadRequest) return } var req ReorderCollectionRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid JSON", http.StatusBadRequest) return } if len(req.Items) == 0 { http.Error(w, "Items array cannot be empty", http.StatusBadRequest) return } userInfo, authErr := h.authService.ExtractUserFromRequest(r) if authErr != nil { http.Error(w, "Authentication required", http.StatusUnauthorized) return } // Use repository for reordering err := h.repository.ReorderCollectionItems(context.Background(), siteID, collectionID, req.Items, userInfo.ID) if err != nil { http.Error(w, fmt.Sprintf("Failed to reorder collection: %v", err), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") response := map[string]interface{}{ "success": true, "message": fmt.Sprintf("Successfully reordered %d items", len(req.Items)), } json.NewEncoder(w).Encode(response) } // Stub handlers for remaining endpoints - will implement as needed // GetContentVersions handles GET /api/content/{id}/versions func (h *ContentHandler) GetContentVersions(w http.ResponseWriter, r *http.Request) { http.Error(w, "Content versioning not yet implemented", http.StatusNotImplemented) } // RollbackContent handles POST /api/content/{id}/rollback func (h *ContentHandler) RollbackContent(w http.ResponseWriter, r *http.Request) { http.Error(w, "Content rollback not yet implemented", http.StatusNotImplemented) } // GetAllCollections handles GET /api/collections func (h *ContentHandler) GetAllCollections(w http.ResponseWriter, r *http.Request) { http.Error(w, "Get all collections not yet implemented", http.StatusNotImplemented) } // UpdateCollectionItem handles PUT /api/collections/{id}/items/{item_id} func (h *ContentHandler) UpdateCollectionItem(w http.ResponseWriter, r *http.Request) { http.Error(w, "Update collection item not yet implemented", http.StatusNotImplemented) } // DeleteCollectionItem handles DELETE /api/collections/{id}/items/{item_id} func (h *ContentHandler) DeleteCollectionItem(w http.ResponseWriter, r *http.Request) { http.Error(w, "Delete collection item not yet implemented", http.StatusNotImplemented) } // EnhanceSite handles POST /api/enhance func (h *ContentHandler) EnhanceSite(w http.ResponseWriter, r *http.Request) { http.Error(w, "Site enhancement not yet implemented", http.StatusNotImplemented) } // GetAuthToken handles GET /api/auth/token - provides mock tokens in dev mode func (h *ContentHandler) GetAuthToken(w http.ResponseWriter, r *http.Request) { // Only provide mock tokens in development mode if !h.authService.IsDevMode() { http.Error(w, "Mock authentication only available in development mode", http.StatusForbidden) return } // Generate a mock JWT token mockToken, err := h.authService.CreateMockJWT("dev-user", "dev@localhost", "Development User") if err != nil { http.Error(w, fmt.Sprintf("Failed to create mock token: %v", err), http.StatusInternalServerError) return } response := map[string]interface{}{ "token": mockToken, "token_type": "Bearer", "expires_in": 86400, // 24 hours "user": map[string]string{ "id": "dev-user", "email": "dev@localhost", "name": "Development User", }, "dev_mode": true, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } // GetCollection handles GET /api/collections/{id} func (h *ContentHandler) GetCollection(w http.ResponseWriter, r *http.Request) { collectionID := chi.URLParam(r, "id") siteID := r.URL.Query().Get("site_id") if siteID == "" { http.Error(w, "site_id parameter is required", http.StatusBadRequest) return } collection, err := h.repository.GetCollection(context.Background(), siteID, collectionID) if err != nil { if err == sql.ErrNoRows { http.Error(w, "Collection not found", http.StatusNotFound) return } http.Error(w, fmt.Sprintf("Database error: %v", err), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(collection) } // GetCollectionItems handles GET /api/collections/{id}/items func (h *ContentHandler) GetCollectionItems(w http.ResponseWriter, r *http.Request) { collectionID := chi.URLParam(r, "id") siteID := r.URL.Query().Get("site_id") if siteID == "" { http.Error(w, "site_id parameter is required", http.StatusBadRequest) return } items, err := h.repository.GetCollectionItems(context.Background(), siteID, collectionID) if err != nil { http.Error(w, fmt.Sprintf("Database error: %v", err), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(items) } // CreateCollectionItem handles POST /api/collections/{id}/items func (h *ContentHandler) CreateCollectionItem(w http.ResponseWriter, r *http.Request) { userInfo, authErr := h.authService.ExtractUserFromRequest(r) if authErr != nil { http.Error(w, "Authentication required", http.StatusUnauthorized) return } collectionID := chi.URLParam(r, "id") var req CreateCollectionItemRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, fmt.Sprintf("Invalid JSON: %v", err), http.StatusBadRequest) return } if req.SiteID == "" || req.CollectionID == "" { req.SiteID = r.URL.Query().Get("site_id") req.CollectionID = collectionID } if req.SiteID == "" { http.Error(w, "site_id is required", http.StatusBadRequest) return } if req.TemplateID == 0 { req.TemplateID = 1 // Default to first template } // Use atomic collection item creation from repository createdItem, err := h.repository.CreateCollectionItemAtomic( context.Background(), req.SiteID, req.CollectionID, req.TemplateID, userInfo.ID, ) if err != nil { http.Error(w, fmt.Sprintf("Failed to create collection item: %v", err), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(createdItem) } // RegisterRoutes registers all the content API routes func (h *ContentHandler) RegisterRoutes(r chi.Router) { r.Route("/api", func(r chi.Router) { // ============================================================================= // CONTENT MANAGEMENT - Individual content items // ============================================================================= r.Route("/content", func(r chi.Router) { // Public routes (no auth required) r.Get("/bulk", h.GetBulkContent) // GET /api/content/bulk?site_id=X&ids=a,b,c r.Get("/{id}", h.GetContent) // GET /api/content/{id}?site_id=X r.Get("/", h.GetAllContent) // GET /api/content?site_id=X // Protected routes (require authentication) r.Group(func(r chi.Router) { r.Use(h.authService.RequireAuth) r.Post("/", h.CreateOrUpdateContent) // POST /api/content (upsert) r.Put("/{id}", h.UpdateContent) // PUT /api/content/{id}?site_id=X r.Delete("/{id}", h.DeleteContent) // DELETE /api/content/{id}?site_id=X // Version control sub-routes r.Get("/{id}/versions", h.GetContentVersions) // GET /api/content/{id}/versions?site_id=X r.Post("/{id}/rollback", h.RollbackContent) // POST /api/content/{id}/rollback }) }) // ============================================================================= // COLLECTION MANAGEMENT - Groups of related content // ============================================================================= r.Route("/collections", func(r chi.Router) { // Public routes r.Get("/", h.GetAllCollections) // GET /api/collections?site_id=X r.Get("/{id}", h.GetCollection) // GET /api/collections/{id}?site_id=X r.Get("/{id}/items", h.GetCollectionItems) // GET /api/collections/{id}/items?site_id=X // Protected routes r.Group(func(r chi.Router) { r.Use(h.authService.RequireAuth) r.Post("/{id}/items", h.CreateCollectionItem) // POST /api/collections/{id}/items r.Put("/{id}/items/{item_id}", h.UpdateCollectionItem) // PUT /api/collections/{id}/items/{item_id} r.Delete("/{id}/items/{item_id}", h.DeleteCollectionItem) // DELETE /api/collections/{id}/items/{item_id}?site_id=X // Bulk operations r.Put("/{id}/reorder", h.ReorderCollection) // PUT /api/collections/{id}/reorder?site_id=X }) }) // ============================================================================= // AUTHENTICATION - Development token endpoint // ============================================================================= r.Route("/auth", func(r chi.Router) { r.Get("/token", h.GetAuthToken) // GET /api/auth/token (dev mode only) }) // ============================================================================= // SITE OPERATIONS - Site-level functionality // ============================================================================= r.Group(func(r chi.Router) { r.Use(h.authService.RequireAuth) r.Post("/enhance", h.EnhanceSite) // POST /api/enhance?site_id=X }) }) }