- Implement complete mushroom foraging blog with chanterelles article - Add rich demonstration of .insertr and .insertr-add functionality - Include comprehensive documentation for future .insertr-content vision - Update project styling and configuration to support blog demo - Enhance engine and API handlers for improved content management
523 lines
17 KiB
Go
523 lines
17 KiB
Go
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
|
|
}
|
|
|
|
// Set collection ID from URL param if not provided in body
|
|
if req.CollectionID == "" {
|
|
req.CollectionID = collectionID
|
|
}
|
|
|
|
// Set site ID from query param if not provided in body
|
|
if req.SiteID == "" {
|
|
req.SiteID = r.URL.Query().Get("site_id")
|
|
}
|
|
|
|
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
|
|
})
|
|
})
|
|
}
|