package api import ( "context" "database/sql" "encoding/json" "fmt" "net/http" "strconv" "strings" "time" "github.com/gorilla/mux" "github.com/insertr/insertr/internal/db" "github.com/insertr/insertr/internal/db/postgresql" "github.com/insertr/insertr/internal/db/sqlite" ) // ContentHandler handles all content-related HTTP requests type ContentHandler struct { database *db.Database } // NewContentHandler creates a new content handler func NewContentHandler(database *db.Database) *ContentHandler { return &ContentHandler{ database: database, } } // GetContent handles GET /api/content/{id} func (h *ContentHandler) GetContent(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) contentID := vars["id"] siteID := r.URL.Query().Get("site_id") if siteID == "" { http.Error(w, "site_id parameter is required", http.StatusBadRequest) return } var content interface{} var err error switch h.database.GetDBType() { case "sqlite3": content, err = h.database.GetSQLiteQueries().GetContent(context.Background(), sqlite.GetContentParams{ ID: contentID, SiteID: siteID, }) case "postgresql": content, err = h.database.GetPostgreSQLQueries().GetContent(context.Background(), postgresql.GetContentParams{ ID: contentID, SiteID: siteID, }) default: http.Error(w, "Unsupported database type", http.StatusInternalServerError) return } 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 } item := h.convertToAPIContent(content) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(item) } // GetAllContent handles GET /api/content 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 } var dbContent interface{} var err error switch h.database.GetDBType() { case "sqlite3": dbContent, err = h.database.GetSQLiteQueries().GetAllContent(context.Background(), siteID) case "postgresql": dbContent, err = h.database.GetPostgreSQLQueries().GetAllContent(context.Background(), siteID) default: http.Error(w, "Unsupported database type", http.StatusInternalServerError) return } if err != nil { http.Error(w, fmt.Sprintf("Database error: %v", err), http.StatusInternalServerError) return } items := h.convertToAPIContentList(dbContent) response := ContentResponse{Content: items} w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } // GetBulkContent handles GET /api/content/bulk func (h *ContentHandler) GetBulkContent(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 } // Parse ids parameter idsParam := r.URL.Query()["ids[]"] if len(idsParam) == 0 { // Try single ids parameter idsStr := r.URL.Query().Get("ids") if idsStr == "" { http.Error(w, "ids parameter is required", http.StatusBadRequest) return } idsParam = strings.Split(idsStr, ",") } var dbContent interface{} var err error switch h.database.GetDBType() { case "sqlite3": dbContent, err = h.database.GetSQLiteQueries().GetBulkContent(context.Background(), sqlite.GetBulkContentParams{ SiteID: siteID, Ids: idsParam, }) case "postgresql": dbContent, err = h.database.GetPostgreSQLQueries().GetBulkContent(context.Background(), postgresql.GetBulkContentParams{ SiteID: siteID, Ids: idsParam, }) default: http.Error(w, "Unsupported database type", http.StatusInternalServerError) return } if err != nil { http.Error(w, fmt.Sprintf("Database error: %v", err), http.StatusInternalServerError) return } items := h.convertToAPIContentList(dbContent) response := ContentResponse{Content: items} w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } // CreateContent handles POST /api/content func (h *ContentHandler) CreateContent(w http.ResponseWriter, r *http.Request) { var req CreateContentRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid JSON", http.StatusBadRequest) return } siteID := r.URL.Query().Get("site_id") if siteID == "" { siteID = req.SiteID // fallback to request body } if siteID == "" { siteID = "default" // final fallback } // Extract user from request (for now, use X-User-ID header or fallback) userID := r.Header.Get("X-User-ID") if userID == "" && req.CreatedBy != "" { userID = req.CreatedBy } if userID == "" { userID = "anonymous" } var content interface{} var err error switch h.database.GetDBType() { case "sqlite3": content, err = h.database.GetSQLiteQueries().CreateContent(context.Background(), sqlite.CreateContentParams{ ID: req.ID, SiteID: siteID, Value: req.Value, Type: req.Type, LastEditedBy: userID, }) case "postgresql": content, err = h.database.GetPostgreSQLQueries().CreateContent(context.Background(), postgresql.CreateContentParams{ ID: req.ID, SiteID: siteID, Value: req.Value, Type: req.Type, LastEditedBy: userID, }) default: http.Error(w, "Unsupported database type", http.StatusInternalServerError) return } if err != nil { http.Error(w, fmt.Sprintf("Failed to create content: %v", err), http.StatusInternalServerError) return } item := h.convertToAPIContent(content) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(item) } // UpdateContent handles PUT /api/content/{id} func (h *ContentHandler) UpdateContent(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) contentID := vars["id"] siteID := r.URL.Query().Get("site_id") if siteID == "" { http.Error(w, "site_id parameter is required", http.StatusBadRequest) return } var req UpdateContentRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid JSON", http.StatusBadRequest) return } // Extract user from request userID := r.Header.Get("X-User-ID") if userID == "" && req.UpdatedBy != "" { userID = req.UpdatedBy } if userID == "" { userID = "anonymous" } // Get current content for version history and type preservation var currentContent interface{} var err error switch h.database.GetDBType() { case "sqlite3": currentContent, err = h.database.GetSQLiteQueries().GetContent(context.Background(), sqlite.GetContentParams{ ID: contentID, SiteID: siteID, }) case "postgresql": currentContent, err = h.database.GetPostgreSQLQueries().GetContent(context.Background(), postgresql.GetContentParams{ ID: contentID, SiteID: siteID, }) default: http.Error(w, "Unsupported database type", http.StatusInternalServerError) return } 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 } // Archive current version before updating err = h.createContentVersion(currentContent) if err != nil { http.Error(w, fmt.Sprintf("Failed to create version: %v", err), http.StatusInternalServerError) return } // Determine content type contentType := req.Type if contentType == "" { contentType = h.getContentType(currentContent) // preserve existing type if not specified } // Update the content var updatedContent interface{} switch h.database.GetDBType() { case "sqlite3": updatedContent, err = h.database.GetSQLiteQueries().UpdateContent(context.Background(), sqlite.UpdateContentParams{ Value: req.Value, Type: contentType, LastEditedBy: userID, ID: contentID, SiteID: siteID, }) case "postgresql": updatedContent, err = h.database.GetPostgreSQLQueries().UpdateContent(context.Background(), postgresql.UpdateContentParams{ Value: req.Value, Type: contentType, LastEditedBy: userID, ID: contentID, SiteID: siteID, }) default: http.Error(w, "Unsupported database type", http.StatusInternalServerError) return } if err != nil { http.Error(w, fmt.Sprintf("Failed to update content: %v", err), http.StatusInternalServerError) return } item := h.convertToAPIContent(updatedContent) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(item) } // DeleteContent handles DELETE /api/content/{id} func (h *ContentHandler) DeleteContent(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) contentID := vars["id"] siteID := r.URL.Query().Get("site_id") if siteID == "" { http.Error(w, "site_id parameter is required", http.StatusBadRequest) return } var err error switch h.database.GetDBType() { case "sqlite3": err = h.database.GetSQLiteQueries().DeleteContent(context.Background(), sqlite.DeleteContentParams{ ID: contentID, SiteID: siteID, }) case "postgresql": err = h.database.GetPostgreSQLQueries().DeleteContent(context.Background(), postgresql.DeleteContentParams{ ID: contentID, SiteID: siteID, }) default: http.Error(w, "Unsupported database type", http.StatusInternalServerError) return } if err != nil { http.Error(w, fmt.Sprintf("Failed to delete content: %v", err), http.StatusInternalServerError) return } w.WriteHeader(http.StatusNoContent) } // GetContentVersions handles GET /api/content/{id}/versions func (h *ContentHandler) GetContentVersions(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) contentID := vars["id"] siteID := r.URL.Query().Get("site_id") if siteID == "" { http.Error(w, "site_id parameter is required", http.StatusBadRequest) return } // Parse limit parameter (default to 10) limit := int64(10) if limitStr := r.URL.Query().Get("limit"); limitStr != "" { if parsedLimit, err := strconv.ParseInt(limitStr, 10, 64); err == nil { limit = parsedLimit } } var dbVersions interface{} var err error switch h.database.GetDBType() { case "sqlite3": dbVersions, err = h.database.GetSQLiteQueries().GetContentVersionHistory(context.Background(), sqlite.GetContentVersionHistoryParams{ ContentID: contentID, SiteID: siteID, LimitCount: limit, }) case "postgresql": // Note: PostgreSQL uses different parameter names due to int32 vs int64 dbVersions, err = h.database.GetPostgreSQLQueries().GetContentVersionHistory(context.Background(), postgresql.GetContentVersionHistoryParams{ ContentID: contentID, SiteID: siteID, LimitCount: int32(limit), }) default: http.Error(w, "Unsupported database type", http.StatusInternalServerError) return } if err != nil { http.Error(w, fmt.Sprintf("Database error: %v", err), http.StatusInternalServerError) return } versions := h.convertToAPIVersionList(dbVersions) response := ContentVersionsResponse{Versions: versions} w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } // RollbackContent handles POST /api/content/{id}/rollback func (h *ContentHandler) RollbackContent(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) contentID := vars["id"] siteID := r.URL.Query().Get("site_id") if siteID == "" { http.Error(w, "site_id parameter is required", http.StatusBadRequest) return } var req RollbackContentRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid JSON", http.StatusBadRequest) return } // Get the target version var targetVersion interface{} var err error switch h.database.GetDBType() { case "sqlite3": targetVersion, err = h.database.GetSQLiteQueries().GetContentVersion(context.Background(), req.VersionID) case "postgresql": targetVersion, err = h.database.GetPostgreSQLQueries().GetContentVersion(context.Background(), int32(req.VersionID)) default: http.Error(w, "Unsupported database type", http.StatusInternalServerError) return } if err != nil { if err == sql.ErrNoRows { http.Error(w, "Version not found", http.StatusNotFound) return } http.Error(w, fmt.Sprintf("Database error: %v", err), http.StatusInternalServerError) return } // Verify the version belongs to the correct content if !h.versionMatches(targetVersion, contentID, siteID) { http.Error(w, "Version does not match content", http.StatusBadRequest) return } // Extract user from request userID := r.Header.Get("X-User-ID") if userID == "" && req.RolledBackBy != "" { userID = req.RolledBackBy } if userID == "" { userID = "anonymous" } // Archive current version before rollback var currentContent interface{} switch h.database.GetDBType() { case "sqlite3": currentContent, err = h.database.GetSQLiteQueries().GetContent(context.Background(), sqlite.GetContentParams{ ID: contentID, SiteID: siteID, }) case "postgresql": currentContent, err = h.database.GetPostgreSQLQueries().GetContent(context.Background(), postgresql.GetContentParams{ ID: contentID, SiteID: siteID, }) default: http.Error(w, "Unsupported database type", http.StatusInternalServerError) return } if err != nil { http.Error(w, fmt.Sprintf("Failed to get current content: %v", err), http.StatusInternalServerError) return } err = h.createContentVersion(currentContent) if err != nil { http.Error(w, fmt.Sprintf("Failed to create version: %v", err), http.StatusInternalServerError) return } // Rollback to target version var updatedContent interface{} switch h.database.GetDBType() { case "sqlite3": sqliteVersion := targetVersion.(sqlite.ContentVersion) updatedContent, err = h.database.GetSQLiteQueries().UpdateContent(context.Background(), sqlite.UpdateContentParams{ Value: sqliteVersion.Value, Type: sqliteVersion.Type, LastEditedBy: userID, ID: contentID, SiteID: siteID, }) case "postgresql": pgVersion := targetVersion.(postgresql.ContentVersion) updatedContent, err = h.database.GetPostgreSQLQueries().UpdateContent(context.Background(), postgresql.UpdateContentParams{ Value: pgVersion.Value, Type: pgVersion.Type, LastEditedBy: userID, ID: contentID, SiteID: siteID, }) default: http.Error(w, "Unsupported database type", http.StatusInternalServerError) return } if err != nil { http.Error(w, fmt.Sprintf("Failed to rollback content: %v", err), http.StatusInternalServerError) return } item := h.convertToAPIContent(updatedContent) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(item) } // Helper functions for type conversion func (h *ContentHandler) convertToAPIContent(content interface{}) ContentItem { switch h.database.GetDBType() { case "sqlite3": c := content.(sqlite.Content) return ContentItem{ ID: c.ID, SiteID: c.SiteID, Value: c.Value, Type: c.Type, CreatedAt: time.Unix(c.CreatedAt, 0), UpdatedAt: time.Unix(c.UpdatedAt, 0), LastEditedBy: c.LastEditedBy, } case "postgresql": c := content.(postgresql.Content) return ContentItem{ ID: c.ID, SiteID: c.SiteID, Value: c.Value, Type: c.Type, CreatedAt: time.Unix(c.CreatedAt, 0), UpdatedAt: time.Unix(c.UpdatedAt, 0), LastEditedBy: c.LastEditedBy, } } return ContentItem{} // Should never happen } func (h *ContentHandler) convertToAPIContentList(contentList interface{}) []ContentItem { switch h.database.GetDBType() { case "sqlite3": list := contentList.([]sqlite.Content) items := make([]ContentItem, len(list)) for i, content := range list { items[i] = h.convertToAPIContent(content) } return items case "postgresql": list := contentList.([]postgresql.Content) items := make([]ContentItem, len(list)) for i, content := range list { items[i] = h.convertToAPIContent(content) } return items } return []ContentItem{} // Should never happen } func (h *ContentHandler) convertToAPIVersionList(versionList interface{}) []ContentVersion { switch h.database.GetDBType() { case "sqlite3": list := versionList.([]sqlite.ContentVersion) versions := make([]ContentVersion, len(list)) for i, version := range list { versions[i] = ContentVersion{ VersionID: version.VersionID, ContentID: version.ContentID, SiteID: version.SiteID, Value: version.Value, Type: version.Type, CreatedAt: time.Unix(version.CreatedAt, 0), CreatedBy: version.CreatedBy, } } return versions case "postgresql": list := versionList.([]postgresql.ContentVersion) versions := make([]ContentVersion, len(list)) for i, version := range list { versions[i] = ContentVersion{ VersionID: int64(version.VersionID), ContentID: version.ContentID, SiteID: version.SiteID, Value: version.Value, Type: version.Type, CreatedAt: time.Unix(version.CreatedAt, 0), CreatedBy: version.CreatedBy, } } return versions } return []ContentVersion{} // Should never happen } func (h *ContentHandler) createContentVersion(content interface{}) error { switch h.database.GetDBType() { case "sqlite3": c := content.(sqlite.Content) return h.database.GetSQLiteQueries().CreateContentVersion(context.Background(), sqlite.CreateContentVersionParams{ ContentID: c.ID, SiteID: c.SiteID, Value: c.Value, Type: c.Type, CreatedBy: c.LastEditedBy, }) case "postgresql": c := content.(postgresql.Content) return h.database.GetPostgreSQLQueries().CreateContentVersion(context.Background(), postgresql.CreateContentVersionParams{ ContentID: c.ID, SiteID: c.SiteID, Value: c.Value, Type: c.Type, CreatedBy: c.LastEditedBy, }) } return fmt.Errorf("unsupported database type") } func (h *ContentHandler) getContentType(content interface{}) string { switch h.database.GetDBType() { case "sqlite3": return content.(sqlite.Content).Type case "postgresql": return content.(postgresql.Content).Type } return "" } func (h *ContentHandler) versionMatches(version interface{}, contentID, siteID string) bool { switch h.database.GetDBType() { case "sqlite3": v := version.(sqlite.ContentVersion) return v.ContentID == contentID && v.SiteID == siteID case "postgresql": v := version.(postgresql.ContentVersion) return v.ContentID == contentID && v.SiteID == siteID } return false }