Replace isolated template previews with live collection reconstruction:
- Frontend now reconstructs collection container with all template variants
- Users click directly on rendered templates in proper CSS context
- Perfect preservation of grid/flex layouts and responsive behavior
- Simplified API: preview endpoint returns container_html + templates for frontend reconstruction
- Enhanced UX: WYSIWYG template selection shows exactly what will be added
- Removed redundant templates endpoint in favor of unified preview approach
Backend changes:
- Add GET /api/collections/{id}/preview endpoint
- Remove GET /api/collections/{id}/templates endpoint
- Return container HTML + templates for frontend reconstruction
Frontend changes:
- Replace isolated template modal with live collection preview
- Add generateLivePreview() method for container reconstruction
- Update CollectionManager to use preview API
- Add interactive CSS styling for template selection
This provides true contextual template selection where CSS inheritance,
grid layouts, and responsive design work perfectly in preview mode.
571 lines
18 KiB
Go
571 lines
18 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)
|
|
}
|
|
|
|
// GetCollectionPreview handles GET /api/collections/{id}/preview
|
|
func (h *ContentHandler) GetCollectionPreview(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
|
|
}
|
|
|
|
// Get collection container
|
|
collection, err := h.repository.GetCollection(context.Background(), siteID, collectionID)
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("Collection not found: %v", err), http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
// Get all templates for this collection
|
|
templates, err := h.repository.GetCollectionTemplates(context.Background(), siteID, collectionID)
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("Templates not found: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
response := map[string]interface{}{
|
|
"collection_id": collectionID,
|
|
"container_html": collection.ContainerHTML,
|
|
"templates": templates,
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(response)
|
|
}
|
|
|
|
// 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 {
|
|
http.Error(w, "template_id is required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Get the specific template by ID
|
|
selectedTemplate, err := h.repository.GetCollectionTemplate(context.Background(), req.TemplateID)
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("Template %d not found: %v", req.TemplateID, err), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Verify template belongs to the requested collection and site
|
|
if selectedTemplate.CollectionID != req.CollectionID || selectedTemplate.SiteID != req.SiteID {
|
|
http.Error(w, fmt.Sprintf("Template %d not found in collection %s", req.TemplateID, req.CollectionID), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Use engine's unified collection item creation
|
|
createdItem, err := h.engine.CreateCollectionItemFromTemplate(
|
|
req.SiteID,
|
|
req.CollectionID,
|
|
req.TemplateID,
|
|
selectedTemplate.HTMLTemplate,
|
|
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) {
|
|
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
|
|
r.Get("/{id}/preview", h.GetCollectionPreview) // GET /api/collections/{id}/preview?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
|
|
})
|
|
})
|
|
}
|