package api import ( "context" "database/sql" "encoding/json" "fmt" "net/http" "strconv" "strings" "time" "github.com/gorilla/mux" "github.com/insertr/insertr/internal/auth" "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 authService *auth.AuthService } // NewContentHandler creates a new content handler func NewContentHandler(database *db.Database, authService *auth.AuthService) *ContentHandler { return &ContentHandler{ database: database, authService: authService, } } // 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 using authentication service userInfo, authErr := h.authService.ExtractUserFromRequest(r) if authErr != nil { http.Error(w, fmt.Sprintf("Authentication error: %v", authErr), http.StatusUnauthorized) return } userID := userInfo.ID 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} with upsert functionality 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 using authentication service userInfo, authErr := h.authService.ExtractUserFromRequest(r) if authErr != nil { http.Error(w, fmt.Sprintf("Authentication error: %v", authErr), http.StatusUnauthorized) return } userID := userInfo.ID // Check if content exists for version history (non-blocking) var existingContent interface{} var contentExists bool switch h.database.GetDBType() { case "sqlite3": existingContent, _ = h.database.GetSQLiteQueries().GetContent(context.Background(), sqlite.GetContentParams{ ID: contentID, SiteID: siteID, }) contentExists = existingContent != nil case "postgresql": existingContent, _ = h.database.GetPostgreSQLQueries().GetContent(context.Background(), postgresql.GetContentParams{ ID: contentID, SiteID: siteID, }) contentExists = existingContent != nil } // Archive existing version before upsert (only if content already exists) if contentExists { if err := h.createContentVersion(existingContent); err != nil { // Log error but don't fail the request - version history is non-critical fmt.Printf("Warning: Failed to create content version: %v\n", err) } } // Determine content type: use provided type, fallback to existing type, default to "text" contentType := req.Type if contentType == "" && contentExists { contentType = h.getContentType(existingContent) } if contentType == "" { contentType = "text" // default type for new content } // Perform upsert operation var upsertedContent interface{} var err error switch h.database.GetDBType() { case "sqlite3": upsertedContent, err = h.database.GetSQLiteQueries().UpsertContent(context.Background(), sqlite.UpsertContentParams{ ID: contentID, SiteID: siteID, Value: req.Value, Type: contentType, LastEditedBy: userID, }) case "postgresql": upsertedContent, err = h.database.GetPostgreSQLQueries().UpsertContent(context.Background(), postgresql.UpsertContentParams{ ID: contentID, SiteID: siteID, Value: req.Value, Type: contentType, LastEditedBy: userID, }) default: http.Error(w, "Unsupported database type", http.StatusInternalServerError) return } if err != nil { http.Error(w, fmt.Sprintf("Failed to upsert content: %v", err), http.StatusInternalServerError) return } item := h.convertToAPIContent(upsertedContent) 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 using authentication service userInfo, authErr := h.authService.ExtractUserFromRequest(r) if authErr != nil { http.Error(w, fmt.Sprintf("Authentication error: %v", authErr), http.StatusUnauthorized) return } userID := userInfo.ID // 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 }