diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d9c1da3..17c30a1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -3,8 +3,6 @@ name: Build and Push Docker Image on: push: branches: - - main - - master - dev paths: - 'Dockerfile' @@ -19,9 +17,6 @@ on: - 'models/**' - 'services/**' - 'ui/**' - - tags: - - 'v*' workflow_dispatch: env: @@ -53,48 +48,14 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - # Get current date for image tags - - name: Get current date - id: date - run: echo "date=$(date +'%Y%m%d')" >> $GITHUB_OUTPUT - # Prepare tags based on branch and version - name: Prepare Docker tags id: docker_tags run: | TAGS="" - - # Add branch-specific tags - if [[ "${{ github.ref_name }}" == "main" || "${{ github.ref_name }}" == "master" ]]; then - # For main/master branch, add latest tag - TAGS="$TAGS ${{ env.DOCKERHUB_IMAGE_NAME }}:latest" - elif [[ "${{ github.ref_name }}" == "dev" ]]; then - # For dev branch - TAGS="$TAGS ${{ env.DOCKERHUB_IMAGE_NAME }}:dev" - elif [[ "${{ github.ref_type }}" == "branch" ]]; then - # For other branches - TAGS="$TAGS ${{ env.DOCKERHUB_IMAGE_NAME }}:${{ github.ref_name }}" + if [[ "${{ github.ref_type }}" == "branch" && "${{ github.ref_name }}" == "dev" ]]; then + TAGS="${{ env.DOCKERHUB_IMAGE_NAME }}:dev" fi - - # Add sha tag for all branches - if [[ "${{ github.ref_type }}" == "branch" ]]; then - TAGS="$TAGS,${{ env.DOCKERHUB_IMAGE_NAME }}:sha-${GITHUB_SHA::7}" - fi - - # Add version tag for tagged releases - if [[ "${{ github.ref_type }}" == "tag" && "${{ github.ref }}" == refs/tags/v* ]]; then - VERSION="${{ github.ref_name }}" - - # Add full version tag - TAGS="$TAGS,${{ env.DOCKERHUB_IMAGE_NAME }}:$VERSION" - fi - - # Add date tag for all builds - TAGS="$TAGS,${{ env.DOCKERHUB_IMAGE_NAME }}:${{ steps.date.outputs.date }}" - - # Remove leading space or comma if present - TAGS=$(echo "$TAGS" | sed 's/^[ ,]*//') - echo "tags=$TAGS" >> $GITHUB_OUTPUT echo "Docker tags: $TAGS" @@ -102,8 +63,8 @@ jobs: uses: docker/build-push-action@v5 with: context: . - push: true + push: ${{ steps.docker_tags.outputs.tags != '' }} # IMPORTANT: Only push if tags were generated platforms: linux/amd64,linux/arm64 tags: ${{ steps.docker_tags.outputs.tags }} cache-from: type=gha - cache-to: type=gha,mode=max \ No newline at end of file + cache-to: type=gha,mode=max diff --git a/api/errors/errors.go b/api/errors/errors.go new file mode 100644 index 0000000..288beef --- /dev/null +++ b/api/errors/errors.go @@ -0,0 +1,90 @@ +package errors + +import ( + "log" + "net/http" + + "github.com/gin-gonic/gin" +) + +// APIError represents a standardized error response +type APIError struct { + Code int `json:"code"` + Message string `json:"message"` + Details string `json:"details,omitempty"` // Optional detailed error info +} + +// HandleAPIError logs and returns a standardized error response +func HandleAPIError(c *gin.Context, statusCode int, message string, err error) { + // Log the error with details + if err != nil { + log.Printf("API Error: %s - %v", message, err) + + // Respond to the client with error details + c.JSON(statusCode, APIError{ + Code: statusCode, + Message: message, + Details: err.Error(), + }) + } else { + // Log just the message if no specific error is provided + log.Printf("API Error: %s", message) + + // Respond to the client without error details + c.JSON(statusCode, APIError{ + Code: statusCode, + Message: message, + }) + } +} + +// NotFound handles not found errors (404) +func NotFound(c *gin.Context, resourceType, id string) { + message := resourceType + " not found" + if id != "" { + message += ": " + id + } + + HandleAPIError(c, http.StatusNotFound, message, nil) +} + +// BadRequest handles bad request errors (400) +func BadRequest(c *gin.Context, message string, err error) { + HandleAPIError(c, http.StatusBadRequest, message, err) +} + +// ServerError handles internal server errors (500) +func ServerError(c *gin.Context, message string, err error) { + HandleAPIError(c, http.StatusInternalServerError, message, err) +} + +// Unauthorized handles unauthorized errors (401) +func Unauthorized(c *gin.Context, message string) { + if message == "" { + message = "Unauthorized access" + } + HandleAPIError(c, http.StatusUnauthorized, message, nil) +} + +// Forbidden handles forbidden errors (403) +func Forbidden(c *gin.Context, message string) { + if message == "" { + message = "Access forbidden" + } + HandleAPIError(c, http.StatusForbidden, message, nil) +} + +// Conflict handles conflict errors (409) +func Conflict(c *gin.Context, message string, err error) { + HandleAPIError(c, http.StatusConflict, message, err) +} + +// UnprocessableEntity handles validation errors (422) +func UnprocessableEntity(c *gin.Context, message string, err error) { + HandleAPIError(c, http.StatusUnprocessableEntity, message, err) +} + +// ServiceUnavailable handles service unavailable errors (503) +func ServiceUnavailable(c *gin.Context, message string, err error) { + HandleAPIError(c, http.StatusServiceUnavailable, message, err) +} \ No newline at end of file diff --git a/api/handlers.go b/api/handlers.go deleted file mode 100644 index 32c4f5c..0000000 --- a/api/handlers.go +++ /dev/null @@ -1,1396 +0,0 @@ -package api - -import ( - "crypto/rand" - "database/sql" - "encoding/hex" - "encoding/json" - "fmt" - "log" - "net/http" - "strings" - "time" - - "github.com/gin-gonic/gin" -) - -// APIError represents a standardized error response -type APIError struct { - Code int `json:"code"` - Message string `json:"message"` -} - -// ResponseWithError sends a standardized error response -func ResponseWithError(c *gin.Context, statusCode int, message string) { - c.JSON(statusCode, APIError{ - Code: statusCode, - Message: message, - }) -} - -// getMiddlewares returns all middleware configurations -func (s *Server) getMiddlewares(c *gin.Context) { - middlewares, err := s.db.GetMiddlewares() - if err != nil { - log.Printf("Error fetching middlewares: %v", err) - ResponseWithError(c, http.StatusInternalServerError, "Failed to fetch middlewares") - return - } - c.JSON(http.StatusOK, middlewares) -} - -// ensureEmptyStringsPreserved ensures empty strings are properly preserved in middleware configs -func ensureEmptyStringsPreserved(config map[string]interface{}) map[string]interface{} { - if config == nil { - return nil - } - - // Process custom headers which commonly use empty strings - if customResponseHeaders, ok := config["customResponseHeaders"].(map[string]interface{}); ok { - for key, value := range customResponseHeaders { - // Ensure nil or null values are converted to empty strings where appropriate - if value == nil { - customResponseHeaders[key] = "" - } - } - } - - if customRequestHeaders, ok := config["customRequestHeaders"].(map[string]interface{}); ok { - for key, value := range customRequestHeaders { - if value == nil { - customRequestHeaders[key] = "" - } - } - } - - // Common header fields that might have empty strings - headerFields := []string{ - "Server", "X-Powered-By", "customFrameOptionsValue", - "contentSecurityPolicy", "referrerPolicy", "permissionsPolicy", - } - - for _, field := range headerFields { - if value, exists := config[field]; exists && value == nil { - config[field] = "" - } - } - - // Return the processed config - return config -} - -// createMiddleware creates a new middleware configuration -func (s *Server) createMiddleware(c *gin.Context) { - var middleware struct { - Name string `json:"name" binding:"required"` - Type string `json:"type" binding:"required"` - Config map[string]interface{} `json:"config" binding:"required"` - } - - if err := c.ShouldBindJSON(&middleware); err != nil { - ResponseWithError(c, http.StatusBadRequest, fmt.Sprintf("Invalid request: %v", err)) - return - } - - // Validate middleware type - if !isValidMiddlewareType(middleware.Type) { - ResponseWithError(c, http.StatusBadRequest, fmt.Sprintf("Invalid middleware type: %s", middleware.Type)) - return - } - - // Generate a unique ID - id, err := generateID() - if err != nil { - log.Printf("Error generating ID: %v", err) - ResponseWithError(c, http.StatusInternalServerError, "Failed to generate ID") - return - } - - // Ensure empty strings are properly preserved in config - middleware.Config = ensureEmptyStringsPreserved(middleware.Config) - - // Convert config to JSON string - configJSON, err := json.Marshal(middleware.Config) - if err != nil { - log.Printf("Error encoding config: %v", err) - ResponseWithError(c, http.StatusInternalServerError, "Failed to encode config") - return - } - - // Insert into database using a transaction - tx, err := s.db.Begin() - if err != nil { - log.Printf("Error beginning transaction: %v", err) - ResponseWithError(c, http.StatusInternalServerError, "Database error") - return - } - - // If something goes wrong, rollback - var txErr error - defer func() { - if txErr != nil { - tx.Rollback() - log.Printf("Transaction rolled back due to error: %v", txErr) - } - }() - - log.Printf("Attempting to insert middleware with ID=%s, name=%s, type=%s", - id, middleware.Name, middleware.Type) - - result, txErr := tx.Exec( - "INSERT INTO middlewares (id, name, type, config) VALUES (?, ?, ?, ?)", - id, middleware.Name, middleware.Type, string(configJSON), - ) - - if txErr != nil { - log.Printf("Error inserting middleware: %v", txErr) - ResponseWithError(c, http.StatusInternalServerError, "Failed to save middleware") - return - } - - rowsAffected, err := result.RowsAffected() - if err == nil { - log.Printf("Insert affected %d rows", rowsAffected) - } - - // Commit the transaction - if txErr = tx.Commit(); txErr != nil { - log.Printf("Error committing transaction: %v", txErr) - ResponseWithError(c, http.StatusInternalServerError, "Database error") - return - } - - log.Printf("Successfully created middleware %s (%s)", middleware.Name, id) - c.JSON(http.StatusCreated, gin.H{ - "id": id, - "name": middleware.Name, - "type": middleware.Type, - "config": middleware.Config, - }) -} - -// getMiddleware returns a specific middleware configuration -func (s *Server) getMiddleware(c *gin.Context) { - id := c.Param("id") - if id == "" { - ResponseWithError(c, http.StatusBadRequest, "Middleware ID is required") - return - } - - middleware, err := s.db.GetMiddleware(id) - if err != nil { - if err.Error() == fmt.Sprintf("middleware not found: %s", id) { - ResponseWithError(c, http.StatusNotFound, "Middleware not found") - return - } - log.Printf("Error fetching middleware: %v", err) - ResponseWithError(c, http.StatusInternalServerError, "Failed to fetch middleware") - return - } - - c.JSON(http.StatusOK, middleware) -} - -// updateRouterPriority updates the router priority for a resource -func (s *Server) updateRouterPriority(c *gin.Context) { - id := c.Param("id") - if id == "" { - ResponseWithError(c, http.StatusBadRequest, "Resource ID is required") - return - } - - var input struct { - RouterPriority int `json:"router_priority" binding:"required"` - } - - if err := c.ShouldBindJSON(&input); err != nil { - ResponseWithError(c, http.StatusBadRequest, fmt.Sprintf("Invalid request: %v", err)) - return - } - - // Verify resource exists and is active - var exists int - var status string - err := s.db.QueryRow("SELECT 1, status FROM resources WHERE id = ?", id).Scan(&exists, &status) - if err == sql.ErrNoRows { - ResponseWithError(c, http.StatusNotFound, "Resource not found") - return - } else if err != nil { - log.Printf("Error checking resource existence: %v", err) - ResponseWithError(c, http.StatusInternalServerError, "Database error") - return - } - - // Don't allow updating disabled resources - if status == "disabled" { - ResponseWithError(c, http.StatusBadRequest, "Cannot update a disabled resource") - return - } - - // Update the resource within a transaction - tx, err := s.db.Begin() - if err != nil { - log.Printf("Error beginning transaction: %v", err) - ResponseWithError(c, http.StatusInternalServerError, "Database error") - return - } - - var txErr error - defer func() { - if txErr != nil { - tx.Rollback() - log.Printf("Transaction rolled back due to error: %v", txErr) - } - }() - - log.Printf("Updating router priority for resource %s to %d", id, input.RouterPriority) - - result, txErr := tx.Exec( - "UPDATE resources SET router_priority = ?, updated_at = ? WHERE id = ?", - input.RouterPriority, time.Now(), id, - ) - - if txErr != nil { - log.Printf("Error updating router priority: %v", txErr) - ResponseWithError(c, http.StatusInternalServerError, "Failed to update router priority") - return - } - - rowsAffected, err := result.RowsAffected() - if err == nil { - log.Printf("Update affected %d rows", rowsAffected) - } - - // Commit the transaction - if txErr = tx.Commit(); txErr != nil { - log.Printf("Error committing transaction: %v", txErr) - ResponseWithError(c, http.StatusInternalServerError, "Database error") - return - } - - log.Printf("Successfully updated router priority for resource %s", id) - c.JSON(http.StatusOK, gin.H{ - "id": id, - "router_priority": input.RouterPriority, - }) -} - -// updateMiddleware updates a middleware configuration -func (s *Server) updateMiddleware(c *gin.Context) { - id := c.Param("id") - if id == "" { - ResponseWithError(c, http.StatusBadRequest, "Middleware ID is required") - return - } - - var middleware struct { - Name string `json:"name" binding:"required"` - Type string `json:"type" binding:"required"` - Config map[string]interface{} `json:"config" binding:"required"` - } - - if err := c.ShouldBindJSON(&middleware); err != nil { - ResponseWithError(c, http.StatusBadRequest, fmt.Sprintf("Invalid request: %v", err)) - return - } - - // Validate middleware type - if !isValidMiddlewareType(middleware.Type) { - ResponseWithError(c, http.StatusBadRequest, fmt.Sprintf("Invalid middleware type: %s", middleware.Type)) - return - } - - // Check if middleware exists - var exists int - err := s.db.QueryRow("SELECT 1 FROM middlewares WHERE id = ?", id).Scan(&exists) - if err == sql.ErrNoRows { - ResponseWithError(c, http.StatusNotFound, "Middleware not found") - return - } else if err != nil { - log.Printf("Error checking middleware existence: %v", err) - ResponseWithError(c, http.StatusInternalServerError, "Database error") - return - } - - // Ensure empty strings are properly preserved in config - middleware.Config = ensureEmptyStringsPreserved(middleware.Config) - - // Convert config to JSON string - configJSON, err := json.Marshal(middleware.Config) - if err != nil { - log.Printf("Error encoding config: %v", err) - ResponseWithError(c, http.StatusInternalServerError, "Failed to encode config") - return - } - - // Update in database using a transaction - tx, err := s.db.Begin() - if err != nil { - log.Printf("Error beginning transaction: %v", err) - ResponseWithError(c, http.StatusInternalServerError, "Database error") - return - } - - // If something goes wrong, rollback - var txErr error - defer func() { - if txErr != nil { - tx.Rollback() - log.Printf("Transaction rolled back due to error: %v", txErr) - } - }() - - log.Printf("Attempting to update middleware %s with name=%s, type=%s", - id, middleware.Name, middleware.Type) - - result, txErr := tx.Exec( - "UPDATE middlewares SET name = ?, type = ?, config = ?, updated_at = ? WHERE id = ?", - middleware.Name, middleware.Type, string(configJSON), time.Now(), id, - ) - - if txErr != nil { - log.Printf("Error updating middleware: %v", txErr) - ResponseWithError(c, http.StatusInternalServerError, "Failed to update middleware") - return - } - - rowsAffected, err := result.RowsAffected() - if err == nil { - log.Printf("Update affected %d rows", rowsAffected) - if rowsAffected == 0 { - log.Printf("Warning: Update query succeeded but no rows were affected") - } - } - - // Commit the transaction - if txErr = tx.Commit(); txErr != nil { - log.Printf("Error committing transaction: %v", txErr) - ResponseWithError(c, http.StatusInternalServerError, "Database error") - return - } - - // Double-check that the middleware was updated - var updatedName string - err = s.db.QueryRow("SELECT name FROM middlewares WHERE id = ?", id).Scan(&updatedName) - if err != nil { - log.Printf("Warning: Could not verify middleware update: %v", err) - } else if updatedName != middleware.Name { - log.Printf("Warning: Name mismatch after update. Expected '%s', got '%s'", middleware.Name, updatedName) - } else { - log.Printf("Successfully verified middleware update for %s", id) - } - - // Return the updated middleware - c.JSON(http.StatusOK, gin.H{ - "id": id, - "name": middleware.Name, - "type": middleware.Type, - "config": middleware.Config, - }) -} - -// sanitizeMiddlewareConfig ensures proper formatting of duration values and strings -func sanitizeMiddlewareConfig(config map[string]interface{}) { - // List of keys that should be treated as duration values - durationKeys := map[string]bool{ - "checkPeriod": true, - "fallbackDuration": true, - "recoveryDuration": true, - "initialInterval": true, - "retryTimeout": true, - "gracePeriod": true, - } - - // Process the configuration recursively - sanitizeConfigRecursive(config, durationKeys) -} - -// sanitizeConfigRecursive processes config values recursively -func sanitizeConfigRecursive(data interface{}, durationKeys map[string]bool) { - // Process based on data type - switch v := data.(type) { - case map[string]interface{}: - // Process each key-value pair in the map - for key, value := range v { - // Handle different value types - switch innerVal := value.(type) { - case string: - // Check if this is a duration field and ensure proper format - if durationKeys[key] { - // Check if the string has extra quotes - if len(innerVal) > 2 && strings.HasPrefix(innerVal, "\"") && strings.HasSuffix(innerVal, "\"") { - // Remove the extra quotes - v[key] = strings.Trim(innerVal, "\"") - } - } - case map[string]interface{}, []interface{}: - // Recursively process nested structures - sanitizeConfigRecursive(innerVal, durationKeys) - } - } - case []interface{}: - // Process each item in the array - for i, item := range v { - switch innerVal := item.(type) { - case map[string]interface{}, []interface{}: - // Recursively process nested structures - sanitizeConfigRecursive(innerVal, durationKeys) - case string: - // Check if string has unnecessary quotes - if len(innerVal) > 2 && strings.HasPrefix(innerVal, "\"") && strings.HasSuffix(innerVal, "\"") { - v[i] = strings.Trim(innerVal, "\"") - } - } - } - } -} - -// deleteMiddleware deletes a middleware configuration -func (s *Server) deleteMiddleware(c *gin.Context) { - id := c.Param("id") - if id == "" { - ResponseWithError(c, http.StatusBadRequest, "Middleware ID is required") - return - } - - // Check for dependencies first - var count int - err := s.db.QueryRow("SELECT COUNT(*) FROM resource_middlewares WHERE middleware_id = ?", id).Scan(&count) - if err != nil { - log.Printf("Error checking middleware dependencies: %v", err) - ResponseWithError(c, http.StatusInternalServerError, "Database error") - return - } - - if count > 0 { - ResponseWithError(c, http.StatusConflict, fmt.Sprintf("Cannot delete middleware because it is used by %d resources", count)) - return - } - - // Delete from database using a transaction - tx, err := s.db.Begin() - if err != nil { - log.Printf("Error beginning transaction: %v", err) - ResponseWithError(c, http.StatusInternalServerError, "Database error") - return - } - - // If something goes wrong, rollback - var txErr error - defer func() { - if txErr != nil { - tx.Rollback() - log.Printf("Transaction rolled back due to error: %v", txErr) - } - }() - - log.Printf("Attempting to delete middleware %s", id) - - result, txErr := tx.Exec("DELETE FROM middlewares WHERE id = ?", id) - if txErr != nil { - log.Printf("Error deleting middleware: %v", txErr) - ResponseWithError(c, http.StatusInternalServerError, "Failed to delete middleware") - return - } - - rowsAffected, err := result.RowsAffected() - if err != nil { - log.Printf("Error getting rows affected: %v", err) - ResponseWithError(c, http.StatusInternalServerError, "Database error") - return - } - - if rowsAffected == 0 { - ResponseWithError(c, http.StatusNotFound, "Middleware not found") - return - } - - log.Printf("Delete affected %d rows", rowsAffected) - - // Commit the transaction - if txErr = tx.Commit(); txErr != nil { - log.Printf("Error committing transaction: %v", txErr) - ResponseWithError(c, http.StatusInternalServerError, "Database error") - return - } - - log.Printf("Successfully deleted middleware %s", id) - c.JSON(http.StatusOK, gin.H{"message": "Middleware deleted successfully"}) -} - -// getResources returns all resources and their assigned middlewares -func (s *Server) getResources(c *gin.Context) { - resources, err := s.db.GetResources() - if err != nil { - log.Printf("Error fetching resources: %v", err) - ResponseWithError(c, http.StatusInternalServerError, "Failed to fetch resources") - return - } - c.JSON(http.StatusOK, resources) -} - -// getResource returns a specific resource -func (s *Server) getResource(c *gin.Context) { - id := c.Param("id") - if id == "" { - ResponseWithError(c, http.StatusBadRequest, "Resource ID is required") - return - } - - resource, err := s.db.GetResource(id) - if err != nil { - if err.Error() == fmt.Sprintf("resource not found: %s", id) { - ResponseWithError(c, http.StatusNotFound, "Resource not found") - return - } - log.Printf("Error fetching resource: %v", err) - ResponseWithError(c, http.StatusInternalServerError, "Failed to fetch resource") - return - } - - c.JSON(http.StatusOK, resource) -} - -// deleteResource deletes a resource from the database -func (s *Server) deleteResource(c *gin.Context) { - id := c.Param("id") - if id == "" { - ResponseWithError(c, http.StatusBadRequest, "Resource ID is required") - return - } - - // Check if resource exists and its status - var status string - err := s.db.QueryRow("SELECT status FROM resources WHERE id = ?", id).Scan(&status) - if err == sql.ErrNoRows { - ResponseWithError(c, http.StatusNotFound, "Resource not found") - return - } else if err != nil { - log.Printf("Error checking resource existence: %v", err) - ResponseWithError(c, http.StatusInternalServerError, "Database error") - return - } - - // Only allow deletion of disabled resources - if status != "disabled" { - ResponseWithError(c, http.StatusBadRequest, "Only disabled resources can be deleted") - return - } - - // Delete the resource using a transaction - tx, err := s.db.Begin() - if err != nil { - log.Printf("Error beginning transaction: %v", err) - ResponseWithError(c, http.StatusInternalServerError, "Database error") - return - } - - // If something goes wrong, rollback - var txErr error - defer func() { - if txErr != nil { - tx.Rollback() - log.Printf("Transaction rolled back due to error: %v", txErr) - } - }() - - // First delete any middleware relationships - log.Printf("Removing middleware relationships for resource %s", id) - _, txErr = tx.Exec("DELETE FROM resource_middlewares WHERE resource_id = ?", id) - if txErr != nil { - log.Printf("Error removing resource middlewares: %v", txErr) - ResponseWithError(c, http.StatusInternalServerError, "Failed to delete resource") - return - } - - // Then delete the resource - log.Printf("Deleting resource %s", id) - result, txErr := tx.Exec("DELETE FROM resources WHERE id = ?", id) - if txErr != nil { - log.Printf("Error deleting resource: %v", txErr) - ResponseWithError(c, http.StatusInternalServerError, "Failed to delete resource") - return - } - - rowsAffected, err := result.RowsAffected() - if err != nil { - log.Printf("Error getting rows affected: %v", err) - ResponseWithError(c, http.StatusInternalServerError, "Database error") - return - } - - if rowsAffected == 0 { - ResponseWithError(c, http.StatusNotFound, "Resource not found") - return - } - - log.Printf("Delete affected %d rows", rowsAffected) - - // Commit the transaction - if txErr = tx.Commit(); txErr != nil { - log.Printf("Error committing transaction: %v", txErr) - ResponseWithError(c, http.StatusInternalServerError, "Database error") - return - } - - log.Printf("Successfully deleted resource %s", id) - c.JSON(http.StatusOK, gin.H{"message": "Resource deleted successfully"}) -} - -// assignMiddleware assigns a middleware to a resource -func (s *Server) assignMiddleware(c *gin.Context) { - resourceID := c.Param("id") - if resourceID == "" { - ResponseWithError(c, http.StatusBadRequest, "Resource ID is required") - return - } - - var input struct { - MiddlewareID string `json:"middleware_id" binding:"required"` - Priority int `json:"priority"` - } - - if err := c.ShouldBindJSON(&input); err != nil { - ResponseWithError(c, http.StatusBadRequest, fmt.Sprintf("Invalid request: %v", err)) - return - } - - // Default priority is 100 if not specified - if input.Priority <= 0 { - input.Priority = 100 - } - - // Verify resource exists - var exists int - var status string - err := s.db.QueryRow("SELECT 1, status FROM resources WHERE id = ?", resourceID).Scan(&exists, &status) - if err == sql.ErrNoRows { - ResponseWithError(c, http.StatusNotFound, "Resource not found") - return - } else if err != nil { - log.Printf("Error checking resource existence: %v", err) - ResponseWithError(c, http.StatusInternalServerError, "Database error") - return - } - - // Don't allow attaching middlewares to disabled resources - if status == "disabled" { - ResponseWithError(c, http.StatusBadRequest, "Cannot assign middleware to a disabled resource") - return - } - - // Verify middleware exists - err = s.db.QueryRow("SELECT 1 FROM middlewares WHERE id = ?", input.MiddlewareID).Scan(&exists) - if err == sql.ErrNoRows { - ResponseWithError(c, http.StatusNotFound, "Middleware not found") - return - } else if err != nil { - log.Printf("Error checking middleware existence: %v", err) - ResponseWithError(c, http.StatusInternalServerError, "Database error") - return - } - - // Insert or update the resource middleware relationship using a transaction - tx, err := s.db.Begin() - if err != nil { - log.Printf("Error beginning transaction: %v", err) - ResponseWithError(c, http.StatusInternalServerError, "Database error") - return - } - - // If something goes wrong, rollback - var txErr error - defer func() { - if txErr != nil { - tx.Rollback() - log.Printf("Transaction rolled back due to error: %v", txErr) - } - }() - - // First delete any existing relationship - log.Printf("Removing existing middleware relationship: resource=%s, middleware=%s", - resourceID, input.MiddlewareID) - _, txErr = tx.Exec( - "DELETE FROM resource_middlewares WHERE resource_id = ? AND middleware_id = ?", - resourceID, input.MiddlewareID, - ) - if txErr != nil { - log.Printf("Error removing existing relationship: %v", txErr) - ResponseWithError(c, http.StatusInternalServerError, "Database error") - return - } - - // Then insert the new relationship - log.Printf("Creating new middleware relationship: resource=%s, middleware=%s, priority=%d", - resourceID, input.MiddlewareID, input.Priority) - result, txErr := tx.Exec( - "INSERT INTO resource_middlewares (resource_id, middleware_id, priority) VALUES (?, ?, ?)", - resourceID, input.MiddlewareID, input.Priority, - ) - if txErr != nil { - log.Printf("Error assigning middleware: %v", txErr) - ResponseWithError(c, http.StatusInternalServerError, "Failed to assign middleware") - return - } - - rowsAffected, err := result.RowsAffected() - if err == nil { - log.Printf("Insert affected %d rows", rowsAffected) - } - - // Commit the transaction - if txErr = tx.Commit(); txErr != nil { - log.Printf("Error committing transaction: %v", txErr) - ResponseWithError(c, http.StatusInternalServerError, "Database error") - return - } - - log.Printf("Successfully assigned middleware %s to resource %s with priority %d", - input.MiddlewareID, resourceID, input.Priority) - c.JSON(http.StatusOK, gin.H{ - "resource_id": resourceID, - "middleware_id": input.MiddlewareID, - "priority": input.Priority, - }) -} - -// assignMultipleMiddlewares assigns multiple middlewares to a resource in one operation -func (s *Server) assignMultipleMiddlewares(c *gin.Context) { - resourceID := c.Param("id") - if resourceID == "" { - ResponseWithError(c, http.StatusBadRequest, "Resource ID is required") - return - } - - var input struct { - Middlewares []struct { - MiddlewareID string `json:"middleware_id" binding:"required"` - Priority int `json:"priority"` - } `json:"middlewares" binding:"required"` - } - - if err := c.ShouldBindJSON(&input); err != nil { - ResponseWithError(c, http.StatusBadRequest, fmt.Sprintf("Invalid request: %v", err)) - return - } - - // Verify resource exists and is active - var exists int - var status string - err := s.db.QueryRow("SELECT 1, status FROM resources WHERE id = ?", resourceID).Scan(&exists, &status) - if err == sql.ErrNoRows { - ResponseWithError(c, http.StatusNotFound, "Resource not found") - return - } else if err != nil { - log.Printf("Error checking resource existence: %v", err) - ResponseWithError(c, http.StatusInternalServerError, "Database error") - return - } - - // Don't allow attaching middlewares to disabled resources - if status == "disabled" { - ResponseWithError(c, http.StatusBadRequest, "Cannot assign middlewares to a disabled resource") - return - } - - // Start a transaction - tx, err := s.db.Begin() - if err != nil { - log.Printf("Error beginning transaction: %v", err) - ResponseWithError(c, http.StatusInternalServerError, "Database error") - return - } - - // If something goes wrong, rollback - var txErr error - defer func() { - if txErr != nil { - tx.Rollback() - log.Printf("Transaction rolled back due to error: %v", txErr) - } - }() - - // Process each middleware - successful := make([]map[string]interface{}, 0) - log.Printf("Assigning %d middlewares to resource %s", len(input.Middlewares), resourceID) - - for _, mw := range input.Middlewares { - // Default priority is 100 if not specified - if mw.Priority <= 0 { - mw.Priority = 100 - } - - // Verify middleware exists - var middlewareExists int - err := s.db.QueryRow("SELECT 1 FROM middlewares WHERE id = ?", mw.MiddlewareID).Scan(&middlewareExists) - if err == sql.ErrNoRows { - // Skip this middleware but don't fail the entire request - log.Printf("Middleware %s not found, skipping", mw.MiddlewareID) - continue - } else if err != nil { - log.Printf("Error checking middleware existence: %v", err) - ResponseWithError(c, http.StatusInternalServerError, "Database error") - return - } - - // First delete any existing relationship - log.Printf("Removing existing relationship: resource=%s, middleware=%s", - resourceID, mw.MiddlewareID) - _, txErr = tx.Exec( - "DELETE FROM resource_middlewares WHERE resource_id = ? AND middleware_id = ?", - resourceID, mw.MiddlewareID, - ) - if txErr != nil { - log.Printf("Error removing existing relationship: %v", txErr) - ResponseWithError(c, http.StatusInternalServerError, "Database error") - return - } - - // Then insert the new relationship - log.Printf("Creating new relationship: resource=%s, middleware=%s, priority=%d", - resourceID, mw.MiddlewareID, mw.Priority) - result, txErr := tx.Exec( - "INSERT INTO resource_middlewares (resource_id, middleware_id, priority) VALUES (?, ?, ?)", - resourceID, mw.MiddlewareID, mw.Priority, - ) - if txErr != nil { - log.Printf("Error assigning middleware: %v", txErr) - ResponseWithError(c, http.StatusInternalServerError, "Failed to assign middleware") - return - } - - rowsAffected, err := result.RowsAffected() - if err == nil && rowsAffected > 0 { - log.Printf("Successfully assigned middleware %s with priority %d", - mw.MiddlewareID, mw.Priority) - successful = append(successful, map[string]interface{}{ - "middleware_id": mw.MiddlewareID, - "priority": mw.Priority, - }) - } else { - log.Printf("Warning: Insertion query succeeded but affected %d rows", rowsAffected) - } - } - - // Commit the transaction - if txErr = tx.Commit(); txErr != nil { - log.Printf("Error committing transaction: %v", txErr) - ResponseWithError(c, http.StatusInternalServerError, "Database error") - return - } - - log.Printf("Successfully assigned %d middlewares to resource %s", len(successful), resourceID) - c.JSON(http.StatusOK, gin.H{ - "resource_id": resourceID, - "middlewares": successful, - }) -} - -// removeMiddleware removes a middleware from a resource -func (s *Server) removeMiddleware(c *gin.Context) { - resourceID := c.Param("id") - middlewareID := c.Param("middlewareId") - - if resourceID == "" || middlewareID == "" { - ResponseWithError(c, http.StatusBadRequest, "Resource ID and Middleware ID are required") - return - } - - log.Printf("Removing middleware %s from resource %s", middlewareID, resourceID) - - // Delete the relationship using a transaction - tx, err := s.db.Begin() - if err != nil { - log.Printf("Error beginning transaction: %v", err) - ResponseWithError(c, http.StatusInternalServerError, "Database error") - return - } - - // If something goes wrong, rollback - var txErr error - defer func() { - if txErr != nil { - tx.Rollback() - log.Printf("Transaction rolled back due to error: %v", txErr) - } - }() - - result, txErr := tx.Exec( - "DELETE FROM resource_middlewares WHERE resource_id = ? AND middleware_id = ?", - resourceID, middlewareID, - ) - - if txErr != nil { - log.Printf("Error removing middleware: %v", txErr) - ResponseWithError(c, http.StatusInternalServerError, "Failed to remove middleware") - return - } - - rowsAffected, err := result.RowsAffected() - if err != nil { - log.Printf("Error getting rows affected: %v", err) - ResponseWithError(c, http.StatusInternalServerError, "Database error") - return - } - - if rowsAffected == 0 { - log.Printf("No relationship found between resource %s and middleware %s", resourceID, middlewareID) - ResponseWithError(c, http.StatusNotFound, "Resource middleware relationship not found") - return - } - - log.Printf("Delete affected %d rows", rowsAffected) - - // Commit the transaction - if txErr = tx.Commit(); txErr != nil { - log.Printf("Error committing transaction: %v", txErr) - ResponseWithError(c, http.StatusInternalServerError, "Database error") - return - } - - log.Printf("Successfully removed middleware %s from resource %s", middlewareID, resourceID) - c.JSON(http.StatusOK, gin.H{"message": "Middleware removed from resource successfully"}) -} - -// updateHTTPConfig updates the HTTP router entrypoints configuration -func (s *Server) updateHTTPConfig(c *gin.Context) { - id := c.Param("id") - if id == "" { - ResponseWithError(c, http.StatusBadRequest, "Resource ID is required") - return - } - - var input struct { - Entrypoints string `json:"entrypoints"` - } - - if err := c.ShouldBindJSON(&input); err != nil { - ResponseWithError(c, http.StatusBadRequest, fmt.Sprintf("Invalid request: %v", err)) - return - } - - // Verify resource exists and is active - var exists int - var status string - err := s.db.QueryRow("SELECT 1, status FROM resources WHERE id = ?", id).Scan(&exists, &status) - if err == sql.ErrNoRows { - ResponseWithError(c, http.StatusNotFound, "Resource not found") - return - } else if err != nil { - log.Printf("Error checking resource existence: %v", err) - ResponseWithError(c, http.StatusInternalServerError, "Database error") - return - } - - // Don't allow updating disabled resources - if status == "disabled" { - ResponseWithError(c, http.StatusBadRequest, "Cannot update a disabled resource") - return - } - - // Validate entrypoints - should be comma-separated list - if input.Entrypoints == "" { - input.Entrypoints = "websecure" // Default - } - - // Update the resource within a transaction - tx, err := s.db.Begin() - if err != nil { - log.Printf("Error beginning transaction: %v", err) - ResponseWithError(c, http.StatusInternalServerError, "Database error") - return - } - - var txErr error - defer func() { - if txErr != nil { - tx.Rollback() - log.Printf("Transaction rolled back due to error: %v", txErr) - } - }() - - log.Printf("Updating HTTP entrypoints for resource %s: %s", id, input.Entrypoints) - - result, txErr := tx.Exec( - "UPDATE resources SET entrypoints = ?, updated_at = ? WHERE id = ?", - input.Entrypoints, time.Now(), id, - ) - - if txErr != nil { - log.Printf("Error updating resource entrypoints: %v", txErr) - ResponseWithError(c, http.StatusInternalServerError, "Failed to update resource") - return - } - - rowsAffected, err := result.RowsAffected() - if err == nil { - log.Printf("Update affected %d rows", rowsAffected) - if rowsAffected == 0 { - log.Printf("Warning: Update query succeeded but no rows were affected") - } - } - - // Commit the transaction - if txErr = tx.Commit(); txErr != nil { - log.Printf("Error committing transaction: %v", txErr) - ResponseWithError(c, http.StatusInternalServerError, "Database error") - return - } - - log.Printf("Successfully updated HTTP entrypoints for resource %s", id) - c.JSON(http.StatusOK, gin.H{ - "id": id, - "entrypoints": input.Entrypoints, - }) -} - -// updateTLSConfig updates the TLS certificate domains configuration -func (s *Server) updateTLSConfig(c *gin.Context) { - id := c.Param("id") - if id == "" { - ResponseWithError(c, http.StatusBadRequest, "Resource ID is required") - return - } - - var input struct { - TLSDomains string `json:"tls_domains"` - } - - if err := c.ShouldBindJSON(&input); err != nil { - ResponseWithError(c, http.StatusBadRequest, fmt.Sprintf("Invalid request: %v", err)) - return - } - - // Verify resource exists and is active - var exists int - var status string - err := s.db.QueryRow("SELECT 1, status FROM resources WHERE id = ?", id).Scan(&exists, &status) - if err == sql.ErrNoRows { - ResponseWithError(c, http.StatusNotFound, "Resource not found") - return - } else if err != nil { - log.Printf("Error checking resource existence: %v", err) - ResponseWithError(c, http.StatusInternalServerError, "Database error") - return - } - - // Don't allow updating disabled resources - if status == "disabled" { - ResponseWithError(c, http.StatusBadRequest, "Cannot update a disabled resource") - return - } - - // Update the resource within a transaction - tx, err := s.db.Begin() - if err != nil { - log.Printf("Error beginning transaction: %v", err) - ResponseWithError(c, http.StatusInternalServerError, "Database error") - return - } - - var txErr error - defer func() { - if txErr != nil { - tx.Rollback() - log.Printf("Transaction rolled back due to error: %v", txErr) - } - }() - - log.Printf("Updating TLS domains for resource %s: %s", id, input.TLSDomains) - - result, txErr := tx.Exec( - "UPDATE resources SET tls_domains = ?, updated_at = ? WHERE id = ?", - input.TLSDomains, time.Now(), id, - ) - - if txErr != nil { - log.Printf("Error updating TLS domains: %v", txErr) - ResponseWithError(c, http.StatusInternalServerError, "Failed to update TLS domains") - return - } - - rowsAffected, err := result.RowsAffected() - if err == nil { - log.Printf("Update affected %d rows", rowsAffected) - if rowsAffected == 0 { - log.Printf("Warning: Update query succeeded but no rows were affected") - } - } - - // Commit the transaction - if txErr = tx.Commit(); txErr != nil { - log.Printf("Error committing transaction: %v", txErr) - ResponseWithError(c, http.StatusInternalServerError, "Database error") - return - } - - log.Printf("Successfully updated TLS domains for resource %s", id) - c.JSON(http.StatusOK, gin.H{ - "id": id, - "tls_domains": input.TLSDomains, - }) -} - -// updateTCPConfig updates the TCP SNI router configuration -func (s *Server) updateTCPConfig(c *gin.Context) { - id := c.Param("id") - if id == "" { - ResponseWithError(c, http.StatusBadRequest, "Resource ID is required") - return - } - - var input struct { - TCPEnabled bool `json:"tcp_enabled"` - TCPEntrypoints string `json:"tcp_entrypoints"` - TCPSNIRule string `json:"tcp_sni_rule"` - } - - if err := c.ShouldBindJSON(&input); err != nil { - ResponseWithError(c, http.StatusBadRequest, fmt.Sprintf("Invalid request: %v", err)) - return - } - - // Verify resource exists and is active - var exists int - var status string - err := s.db.QueryRow("SELECT 1, status FROM resources WHERE id = ?", id).Scan(&exists, &status) - if err == sql.ErrNoRows { - ResponseWithError(c, http.StatusNotFound, "Resource not found") - return - } else if err != nil { - log.Printf("Error checking resource existence: %v", err) - ResponseWithError(c, http.StatusInternalServerError, "Database error") - return - } - - // Don't allow updating disabled resources - if status == "disabled" { - ResponseWithError(c, http.StatusBadRequest, "Cannot update a disabled resource") - return - } - - // Validate TCP entrypoints if provided - if input.TCPEntrypoints == "" { - input.TCPEntrypoints = "tcp" // Default - } - - // Validate SNI rule if provided - if input.TCPSNIRule != "" { - // Basic validation - ensure it contains HostSNI - if !strings.Contains(input.TCPSNIRule, "HostSNI") { - ResponseWithError(c, http.StatusBadRequest, "TCP SNI rule must contain HostSNI matcher") - return - } - } - - // Convert boolean to integer for SQLite - tcpEnabled := 0 - if input.TCPEnabled { - tcpEnabled = 1 - } - - // Update the resource within a transaction - tx, err := s.db.Begin() - if err != nil { - log.Printf("Error beginning transaction: %v", err) - ResponseWithError(c, http.StatusInternalServerError, "Database error") - return - } - - var txErr error - defer func() { - if txErr != nil { - tx.Rollback() - log.Printf("Transaction rolled back due to error: %v", txErr) - } - }() - - log.Printf("Updating TCP config for resource %s: enabled=%t, entrypoints=%s", - id, input.TCPEnabled, input.TCPEntrypoints) - - result, txErr := tx.Exec( - "UPDATE resources SET tcp_enabled = ?, tcp_entrypoints = ?, tcp_sni_rule = ?, updated_at = ? WHERE id = ?", - tcpEnabled, input.TCPEntrypoints, input.TCPSNIRule, time.Now(), id, - ) - - if txErr != nil { - log.Printf("Error updating TCP config: %v", txErr) - ResponseWithError(c, http.StatusInternalServerError, "Failed to update TCP configuration") - return - } - - rowsAffected, err := result.RowsAffected() - if err == nil { - log.Printf("Update affected %d rows", rowsAffected) - if rowsAffected == 0 { - log.Printf("Warning: Update query succeeded but no rows were affected") - } - } - - // Commit the transaction - if txErr = tx.Commit(); txErr != nil { - log.Printf("Error committing transaction: %v", txErr) - ResponseWithError(c, http.StatusInternalServerError, "Database error") - return - } - - log.Printf("Successfully updated TCP configuration for resource %s", id) - c.JSON(http.StatusOK, gin.H{ - "id": id, - "tcp_enabled": input.TCPEnabled, - "tcp_entrypoints": input.TCPEntrypoints, - "tcp_sni_rule": input.TCPSNIRule, - }) -} - -// updateHeadersConfig updates the custom headers configuration -func (s *Server) updateHeadersConfig(c *gin.Context) { - id := c.Param("id") - if id == "" { - ResponseWithError(c, http.StatusBadRequest, "Resource ID is required") - return - } - - var input struct { - CustomHeaders map[string]string `json:"custom_headers" binding:"required"` - } - - if err := c.ShouldBindJSON(&input); err != nil { - ResponseWithError(c, http.StatusBadRequest, fmt.Sprintf("Invalid request: %v", err)) - return - } - - // Verify resource exists and is active - var exists int - var status string - err := s.db.QueryRow("SELECT 1, status FROM resources WHERE id = ?", id).Scan(&exists, &status) - if err == sql.ErrNoRows { - ResponseWithError(c, http.StatusNotFound, "Resource not found") - return - } else if err != nil { - log.Printf("Error checking resource existence: %v", err) - ResponseWithError(c, http.StatusInternalServerError, "Database error") - return - } - - // Don't allow updating disabled resources - if status == "disabled" { - ResponseWithError(c, http.StatusBadRequest, "Cannot update a disabled resource") - return - } - - // Convert headers to JSON for storage - headersJSON, err := json.Marshal(input.CustomHeaders) - if err != nil { - log.Printf("Error encoding headers: %v", err) - ResponseWithError(c, http.StatusInternalServerError, "Failed to encode headers") - return - } - - // Update the resource within a transaction - tx, err := s.db.Begin() - if err != nil { - log.Printf("Error beginning transaction: %v", err) - ResponseWithError(c, http.StatusInternalServerError, "Database error") - return - } - - var txErr error - defer func() { - if txErr != nil { - tx.Rollback() - log.Printf("Transaction rolled back due to error: %v", txErr) - } - }() - - log.Printf("Updating custom headers for resource %s with %d headers", - id, len(input.CustomHeaders)) - - result, txErr := tx.Exec( - "UPDATE resources SET custom_headers = ?, updated_at = ? WHERE id = ?", - string(headersJSON), time.Now(), id, - ) - - if txErr != nil { - log.Printf("Error updating custom headers: %v", txErr) - ResponseWithError(c, http.StatusInternalServerError, "Failed to update custom headers") - return - } - - rowsAffected, err := result.RowsAffected() - if err == nil { - log.Printf("Update affected %d rows", rowsAffected) - if rowsAffected == 0 { - log.Printf("Warning: Update query succeeded but no rows were affected") - } - } - - // Commit the transaction - if txErr = tx.Commit(); txErr != nil { - log.Printf("Error committing transaction: %v", txErr) - ResponseWithError(c, http.StatusInternalServerError, "Database error") - return - } - - // Verify the update by reading back the custom_headers - var storedHeaders string - verifyErr := s.db.QueryRow("SELECT custom_headers FROM resources WHERE id = ?", id).Scan(&storedHeaders) - if verifyErr != nil { - log.Printf("Warning: Could not verify headers update: %v", verifyErr) - } else if storedHeaders == "" { - log.Printf("Warning: Headers may be empty after update for resource %s", id) - } else { - log.Printf("Successfully verified headers update for resource %s", id) - } - - log.Printf("Successfully updated custom headers for resource %s", id) - c.JSON(http.StatusOK, gin.H{ - "id": id, - "custom_headers": input.CustomHeaders, - }) -} - -// generateID generates a random 16-character hex string -func generateID() (string, error) { - bytes := make([]byte, 8) - if _, err := rand.Read(bytes); err != nil { - return "", fmt.Errorf("failed to generate random bytes: %w", err) - } - return hex.EncodeToString(bytes), nil -} - -// isValidMiddlewareType checks if a middleware type is valid -func isValidMiddlewareType(typ string) bool { - validTypes := map[string]bool{ - // Currently supported types - "basicAuth": true, - "forwardAuth": true, - "ipWhiteList": true, - "rateLimit": true, - "headers": true, - "stripPrefix": true, - "addPrefix": true, - "redirectRegex": true, - "redirectScheme": true, - "chain": true, - "replacepathregex": true, - "replacePathRegex": true, // Adding correct camelCase version - "plugin": true, - // Additional middleware types from templates.yaml - "digestAuth": true, - "ipAllowList": true, - "stripPrefixRegex": true, - "replacePath": true, - "compress": true, - "circuitBreaker": true, - "contentType": true, - "errors": true, - "grpcWeb": true, - "inFlightReq": true, - "passTLSClientCert": true, - "retry": true, - "buffering": true, - } - - return validTypes[typ] -} \ No newline at end of file diff --git a/api/handlers/common.go b/api/handlers/common.go new file mode 100644 index 0000000..9cae822 --- /dev/null +++ b/api/handlers/common.go @@ -0,0 +1,117 @@ +package handlers + +import ( + "crypto/rand" + "encoding/hex" + "fmt" + "log" + "strings" + + "github.com/gin-gonic/gin" +) + +// APIError represents a standardized error response +type APIError struct { + Code int `json:"code"` + Message string `json:"message"` +} + +// ResponseWithError sends a standardized error response +func ResponseWithError(c *gin.Context, statusCode int, message string) { + c.JSON(statusCode, APIError{ + Code: statusCode, + Message: message, + }) +} + +// generateID generates a random 16-character hex string +func generateID() (string, error) { + bytes := make([]byte, 8) + if _, err := rand.Read(bytes); err != nil { + return "", fmt.Errorf("failed to generate random bytes: %w", err) + } + return hex.EncodeToString(bytes), nil +} + +// isValidMiddlewareType checks if a middleware type is valid +func isValidMiddlewareType(typ string) bool { + validTypes := map[string]bool{ + "basicAuth": true, + "forwardAuth": true, + "ipWhiteList": true, + "rateLimit": true, + "headers": true, + "stripPrefix": true, + "addPrefix": true, + "redirectRegex": true, + "redirectScheme": true, + "chain": true, + "replacepathregex": true, + "plugin": true, + } + + return validTypes[typ] +} + +// sanitizeMiddlewareConfig ensures proper formatting of duration values and strings +func sanitizeMiddlewareConfig(config map[string]interface{}) { + // List of keys that should be treated as duration values + durationKeys := map[string]bool{ + "checkPeriod": true, + "fallbackDuration": true, + "recoveryDuration": true, + "initialInterval": true, + "retryTimeout": true, + "gracePeriod": true, + } + + // Process the configuration recursively + sanitizeConfigRecursive(config, durationKeys) +} + +// sanitizeConfigRecursive processes config values recursively +func sanitizeConfigRecursive(data interface{}, durationKeys map[string]bool) { + // Process based on data type + switch v := data.(type) { + case map[string]interface{}: + // Process each key-value pair in the map + for key, value := range v { + // Handle different value types + switch innerVal := value.(type) { + case string: + // Check if this is a duration field and ensure proper format + if durationKeys[key] { + // Check if the string has extra quotes + if len(innerVal) > 2 && strings.HasPrefix(innerVal, "\"") && strings.HasSuffix(innerVal, "\"") { + // Remove the extra quotes + v[key] = strings.Trim(innerVal, "\"") + } + } + case map[string]interface{}, []interface{}: + // Recursively process nested structures + sanitizeConfigRecursive(innerVal, durationKeys) + } + } + case []interface{}: + // Process each item in the array + for i, item := range v { + switch innerVal := item.(type) { + case map[string]interface{}, []interface{}: + // Recursively process nested structures + sanitizeConfigRecursive(innerVal, durationKeys) + case string: + // Check if string has unnecessary quotes + if len(innerVal) > 2 && strings.HasPrefix(innerVal, "\"") && strings.HasSuffix(innerVal, "\"") { + v[i] = strings.Trim(innerVal, "\"") + } + } + } + } +} + +// LogError logs an error with context information +func LogError(context string, err error) { + if err != nil { + log.Printf("Error %s: %v", context, err) + } +} \ No newline at end of file diff --git a/api/handlers/config.go b/api/handlers/config.go new file mode 100644 index 0000000..7b471c2 --- /dev/null +++ b/api/handlers/config.go @@ -0,0 +1,495 @@ +package handlers + +import ( + "database/sql" + "encoding/json" + "fmt" + "log" + "net/http" + "time" + + "github.com/gin-gonic/gin" +) + +// ConfigHandler handles configuration-related requests +type ConfigHandler struct { + DB *sql.DB +} + +// NewConfigHandler creates a new config handler +func NewConfigHandler(db *sql.DB) *ConfigHandler { + return &ConfigHandler{DB: db} +} + +// UpdateRouterPriority updates the router priority for a resource +func (h *ConfigHandler) UpdateRouterPriority(c *gin.Context) { + id := c.Param("id") + if id == "" { + ResponseWithError(c, http.StatusBadRequest, "Resource ID is required") + return + } + + var input struct { + RouterPriority int `json:"router_priority" binding:"required"` + } + + if err := c.ShouldBindJSON(&input); err != nil { + ResponseWithError(c, http.StatusBadRequest, fmt.Sprintf("Invalid request: %v", err)) + return + } + + // Verify resource exists and is active + var exists int + var status string + err := h.DB.QueryRow("SELECT 1, status FROM resources WHERE id = ?", id).Scan(&exists, &status) + if err == sql.ErrNoRows { + ResponseWithError(c, http.StatusNotFound, "Resource not found") + return + } else if err != nil { + log.Printf("Error checking resource existence: %v", err) + ResponseWithError(c, http.StatusInternalServerError, "Database error") + return + } + + // Don't allow updating disabled resources + if status == "disabled" { + ResponseWithError(c, http.StatusBadRequest, "Cannot update a disabled resource") + return + } + + // Update the resource within a transaction + tx, err := h.DB.Begin() + if err != nil { + log.Printf("Error beginning transaction: %v", err) + ResponseWithError(c, http.StatusInternalServerError, "Database error") + return + } + + var txErr error + defer func() { + if txErr != nil { + tx.Rollback() + log.Printf("Transaction rolled back due to error: %v", txErr) + } + }() + + log.Printf("Updating router priority for resource %s to %d", id, input.RouterPriority) + + result, txErr := tx.Exec( + "UPDATE resources SET router_priority = ?, updated_at = ? WHERE id = ?", + input.RouterPriority, time.Now(), id, + ) + + if txErr != nil { + log.Printf("Error updating router priority: %v", txErr) + ResponseWithError(c, http.StatusInternalServerError, "Failed to update router priority") + return + } + + rowsAffected, err := result.RowsAffected() + if err == nil { + log.Printf("Update affected %d rows", rowsAffected) + } + + // Commit the transaction + if txErr = tx.Commit(); txErr != nil { + log.Printf("Error committing transaction: %v", txErr) + ResponseWithError(c, http.StatusInternalServerError, "Database error") + return + } + + log.Printf("Successfully updated router priority for resource %s", id) + c.JSON(http.StatusOK, gin.H{ + "id": id, + "router_priority": input.RouterPriority, + }) +} + +// UpdateHTTPConfig updates the HTTP router entrypoints configuration +func (h *ConfigHandler) UpdateHTTPConfig(c *gin.Context) { + id := c.Param("id") + if id == "" { + ResponseWithError(c, http.StatusBadRequest, "Resource ID is required") + return + } + + var input struct { + Entrypoints string `json:"entrypoints"` + } + + if err := c.ShouldBindJSON(&input); err != nil { + ResponseWithError(c, http.StatusBadRequest, fmt.Sprintf("Invalid request: %v", err)) + return + } + + // Verify resource exists and is active + var exists int + var status string + err := h.DB.QueryRow("SELECT 1, status FROM resources WHERE id = ?", id).Scan(&exists, &status) + if err == sql.ErrNoRows { + ResponseWithError(c, http.StatusNotFound, "Resource not found") + return + } else if err != nil { + log.Printf("Error checking resource existence: %v", err) + ResponseWithError(c, http.StatusInternalServerError, "Database error") + return + } + + // Don't allow updating disabled resources + if status == "disabled" { + ResponseWithError(c, http.StatusBadRequest, "Cannot update a disabled resource") + return + } + + // Validate entrypoints - should be comma-separated list + if input.Entrypoints == "" { + input.Entrypoints = "websecure" // Default + } + + // Update the resource within a transaction + tx, err := h.DB.Begin() + if err != nil { + log.Printf("Error beginning transaction: %v", err) + ResponseWithError(c, http.StatusInternalServerError, "Database error") + return + } + + var txErr error + defer func() { + if txErr != nil { + tx.Rollback() + log.Printf("Transaction rolled back due to error: %v", txErr) + } + }() + + log.Printf("Updating HTTP entrypoints for resource %s: %s", id, input.Entrypoints) + + result, txErr := tx.Exec( + "UPDATE resources SET entrypoints = ?, updated_at = ? WHERE id = ?", + input.Entrypoints, time.Now(), id, + ) + + if txErr != nil { + log.Printf("Error updating resource entrypoints: %v", txErr) + ResponseWithError(c, http.StatusInternalServerError, "Failed to update resource") + return + } + + rowsAffected, err := result.RowsAffected() + if err == nil { + log.Printf("Update affected %d rows", rowsAffected) + if rowsAffected == 0 { + log.Printf("Warning: Update query succeeded but no rows were affected") + } + } + + // Commit the transaction + if txErr = tx.Commit(); txErr != nil { + log.Printf("Error committing transaction: %v", txErr) + ResponseWithError(c, http.StatusInternalServerError, "Database error") + return + } + + log.Printf("Successfully updated HTTP entrypoints for resource %s", id) + c.JSON(http.StatusOK, gin.H{ + "id": id, + "entrypoints": input.Entrypoints, + }) +} + +// UpdateTLSConfig updates the TLS certificate domains configuration +func (h *ConfigHandler) UpdateTLSConfig(c *gin.Context) { + id := c.Param("id") + if id == "" { + ResponseWithError(c, http.StatusBadRequest, "Resource ID is required") + return + } + + var input struct { + TLSDomains string `json:"tls_domains"` + } + + if err := c.ShouldBindJSON(&input); err != nil { + ResponseWithError(c, http.StatusBadRequest, fmt.Sprintf("Invalid request: %v", err)) + return + } + + // Verify resource exists and is active + var exists int + var status string + err := h.DB.QueryRow("SELECT 1, status FROM resources WHERE id = ?", id).Scan(&exists, &status) + if err == sql.ErrNoRows { + ResponseWithError(c, http.StatusNotFound, "Resource not found") + return + } else if err != nil { + log.Printf("Error checking resource existence: %v", err) + ResponseWithError(c, http.StatusInternalServerError, "Database error") + return + } + + // Don't allow updating disabled resources + if status == "disabled" { + ResponseWithError(c, http.StatusBadRequest, "Cannot update a disabled resource") + return + } + + // Update the resource within a transaction + tx, err := h.DB.Begin() + if err != nil { + log.Printf("Error beginning transaction: %v", err) + ResponseWithError(c, http.StatusInternalServerError, "Database error") + return + } + + var txErr error + defer func() { + if txErr != nil { + tx.Rollback() + log.Printf("Transaction rolled back due to error: %v", txErr) + } + }() + + log.Printf("Updating TLS domains for resource %s: %s", id, input.TLSDomains) + + result, txErr := tx.Exec( + "UPDATE resources SET tls_domains = ?, updated_at = ? WHERE id = ?", + input.TLSDomains, time.Now(), id, + ) + + if txErr != nil { + log.Printf("Error updating TLS domains: %v", txErr) + ResponseWithError(c, http.StatusInternalServerError, "Failed to update TLS domains") + return + } + + rowsAffected, err := result.RowsAffected() + if err == nil { + log.Printf("Update affected %d rows", rowsAffected) + if rowsAffected == 0 { + log.Printf("Warning: Update query succeeded but no rows were affected") + } + } + + // Commit the transaction + if txErr = tx.Commit(); txErr != nil { + log.Printf("Error committing transaction: %v", txErr) + ResponseWithError(c, http.StatusInternalServerError, "Database error") + return + } + + log.Printf("Successfully updated TLS domains for resource %s", id) + c.JSON(http.StatusOK, gin.H{ + "id": id, + "tls_domains": input.TLSDomains, + }) +} + +// UpdateTCPConfig updates the TCP SNI router configuration +func (h *ConfigHandler) UpdateTCPConfig(c *gin.Context) { + id := c.Param("id") + if id == "" { + ResponseWithError(c, http.StatusBadRequest, "Resource ID is required") + return + } + + var input struct { + TCPEnabled bool `json:"tcp_enabled"` + TCPEntrypoints string `json:"tcp_entrypoints"` + TCPSNIRule string `json:"tcp_sni_rule"` + } + + if err := c.ShouldBindJSON(&input); err != nil { + ResponseWithError(c, http.StatusBadRequest, fmt.Sprintf("Invalid request: %v", err)) + return + } + + // Verify resource exists and is active + var exists int + var status string + err := h.DB.QueryRow("SELECT 1, status FROM resources WHERE id = ?", id).Scan(&exists, &status) + if err == sql.ErrNoRows { + ResponseWithError(c, http.StatusNotFound, "Resource not found") + return + } else if err != nil { + log.Printf("Error checking resource existence: %v", err) + ResponseWithError(c, http.StatusInternalServerError, "Database error") + return + } + + // Don't allow updating disabled resources + if status == "disabled" { + ResponseWithError(c, http.StatusBadRequest, "Cannot update a disabled resource") + return + } + + // Validate TCP entrypoints if provided + if input.TCPEntrypoints == "" { + input.TCPEntrypoints = "tcp" // Default + } + + // Convert boolean to integer for SQLite + tcpEnabled := 0 + if input.TCPEnabled { + tcpEnabled = 1 + } + + // Update the resource within a transaction + tx, err := h.DB.Begin() + if err != nil { + log.Printf("Error beginning transaction: %v", err) + ResponseWithError(c, http.StatusInternalServerError, "Database error") + return + } + + var txErr error + defer func() { + if txErr != nil { + tx.Rollback() + log.Printf("Transaction rolled back due to error: %v", txErr) + } + }() + + log.Printf("Updating TCP config for resource %s: enabled=%t, entrypoints=%s", + id, input.TCPEnabled, input.TCPEntrypoints) + + result, txErr := tx.Exec( + "UPDATE resources SET tcp_enabled = ?, tcp_entrypoints = ?, tcp_sni_rule = ?, updated_at = ? WHERE id = ?", + tcpEnabled, input.TCPEntrypoints, input.TCPSNIRule, time.Now(), id, + ) + + if txErr != nil { + log.Printf("Error updating TCP config: %v", txErr) + ResponseWithError(c, http.StatusInternalServerError, "Failed to update TCP configuration") + return + } + + rowsAffected, err := result.RowsAffected() + if err == nil { + log.Printf("Update affected %d rows", rowsAffected) + if rowsAffected == 0 { + log.Printf("Warning: Update query succeeded but no rows were affected") + } + } + + // Commit the transaction + if txErr = tx.Commit(); txErr != nil { + log.Printf("Error committing transaction: %v", txErr) + ResponseWithError(c, http.StatusInternalServerError, "Database error") + return + } + + log.Printf("Successfully updated TCP configuration for resource %s", id) + c.JSON(http.StatusOK, gin.H{ + "id": id, + "tcp_enabled": input.TCPEnabled, + "tcp_entrypoints": input.TCPEntrypoints, + "tcp_sni_rule": input.TCPSNIRule, + }) +} + +// UpdateHeadersConfig updates the custom headers configuration +func (h *ConfigHandler) UpdateHeadersConfig(c *gin.Context) { + id := c.Param("id") + if id == "" { + ResponseWithError(c, http.StatusBadRequest, "Resource ID is required") + return + } + + var input struct { + CustomHeaders map[string]string `json:"custom_headers" binding:"required"` + } + + if err := c.ShouldBindJSON(&input); err != nil { + ResponseWithError(c, http.StatusBadRequest, fmt.Sprintf("Invalid request: %v", err)) + return + } + + // Verify resource exists and is active + var exists int + var status string + err := h.DB.QueryRow("SELECT 1, status FROM resources WHERE id = ?", id).Scan(&exists, &status) + if err == sql.ErrNoRows { + ResponseWithError(c, http.StatusNotFound, "Resource not found") + return + } else if err != nil { + log.Printf("Error checking resource existence: %v", err) + ResponseWithError(c, http.StatusInternalServerError, "Database error") + return + } + + // Don't allow updating disabled resources + if status == "disabled" { + ResponseWithError(c, http.StatusBadRequest, "Cannot update a disabled resource") + return + } + + // Convert headers to JSON for storage + headersJSON, err := json.Marshal(input.CustomHeaders) + if err != nil { + log.Printf("Error encoding headers: %v", err) + ResponseWithError(c, http.StatusInternalServerError, "Failed to encode headers") + return + } + + // Update the resource within a transaction + tx, err := h.DB.Begin() + if err != nil { + log.Printf("Error beginning transaction: %v", err) + ResponseWithError(c, http.StatusInternalServerError, "Database error") + return + } + + var txErr error + defer func() { + if txErr != nil { + tx.Rollback() + log.Printf("Transaction rolled back due to error: %v", txErr) + } + }() + + log.Printf("Updating custom headers for resource %s with %d headers", + id, len(input.CustomHeaders)) + + result, txErr := tx.Exec( + "UPDATE resources SET custom_headers = ?, updated_at = ? WHERE id = ?", + string(headersJSON), time.Now(), id, + ) + + if txErr != nil { + log.Printf("Error updating custom headers: %v", txErr) + ResponseWithError(c, http.StatusInternalServerError, "Failed to update custom headers") + return + } + + rowsAffected, err := result.RowsAffected() + if err == nil { + log.Printf("Update affected %d rows", rowsAffected) + if rowsAffected == 0 { + log.Printf("Warning: Update query succeeded but no rows were affected") + } + } + + // Commit the transaction + if txErr = tx.Commit(); txErr != nil { + log.Printf("Error committing transaction: %v", txErr) + ResponseWithError(c, http.StatusInternalServerError, "Database error") + return + } + + // Verify the update by reading back the custom_headers + var storedHeaders string + verifyErr := h.DB.QueryRow("SELECT custom_headers FROM resources WHERE id = ?", id).Scan(&storedHeaders) + if verifyErr != nil { + log.Printf("Warning: Could not verify headers update: %v", verifyErr) + } else if storedHeaders == "" { + log.Printf("Warning: Headers may be empty after update for resource %s", id) + } else { + log.Printf("Successfully verified headers update for resource %s", id) + } + + log.Printf("Successfully updated custom headers for resource %s", id) + c.JSON(http.StatusOK, gin.H{ + "id": id, + "custom_headers": input.CustomHeaders, + }) +} \ No newline at end of file diff --git a/api/handlers/middlewares.go b/api/handlers/middlewares.go new file mode 100644 index 0000000..cbab4fc --- /dev/null +++ b/api/handlers/middlewares.go @@ -0,0 +1,367 @@ +package handlers + +import ( + "database/sql" + "encoding/json" + "fmt" + "log" + "net/http" + "time" + + "github.com/gin-gonic/gin" +) + +// MiddlewareHandler handles middleware-related requests +type MiddlewareHandler struct { + DB *sql.DB +} + +// NewMiddlewareHandler creates a new middleware handler +func NewMiddlewareHandler(db *sql.DB) *MiddlewareHandler { + return &MiddlewareHandler{DB: db} +} + +// GetMiddlewares returns all middleware configurations +func (h *MiddlewareHandler) GetMiddlewares(c *gin.Context) { + rows, err := h.DB.Query("SELECT id, name, type, config FROM middlewares") + if err != nil { + log.Printf("Error fetching middlewares: %v", err) + ResponseWithError(c, http.StatusInternalServerError, "Failed to fetch middlewares") + return + } + defer rows.Close() + + middlewares := []map[string]interface{}{} + for rows.Next() { + var id, name, typ, configStr string + if err := rows.Scan(&id, &name, &typ, &configStr); err != nil { + log.Printf("Error scanning middleware row: %v", err) + continue + } + + var config map[string]interface{} + if err := json.Unmarshal([]byte(configStr), &config); err != nil { + log.Printf("Error parsing middleware config: %v", err) + config = map[string]interface{}{} + } + + middlewares = append(middlewares, map[string]interface{}{ + "id": id, + "name": name, + "type": typ, + "config": config, + }) + } + + if err := rows.Err(); err != nil { + log.Printf("Error iterating middleware rows: %v", err) + ResponseWithError(c, http.StatusInternalServerError, "Database error while fetching middlewares") + return + } + + c.JSON(http.StatusOK, middlewares) +} + +// CreateMiddleware creates a new middleware configuration +func (h *MiddlewareHandler) CreateMiddleware(c *gin.Context) { + var middleware struct { + Name string `json:"name" binding:"required"` + Type string `json:"type" binding:"required"` + Config map[string]interface{} `json:"config" binding:"required"` + } + + if err := c.ShouldBindJSON(&middleware); err != nil { + ResponseWithError(c, http.StatusBadRequest, fmt.Sprintf("Invalid request: %v", err)) + return + } + + // Validate middleware type + if !isValidMiddlewareType(middleware.Type) { + ResponseWithError(c, http.StatusBadRequest, fmt.Sprintf("Invalid middleware type: %s", middleware.Type)) + return + } + + // Generate a unique ID + id, err := generateID() + if err != nil { + log.Printf("Error generating ID: %v", err) + ResponseWithError(c, http.StatusInternalServerError, "Failed to generate ID") + return + } + + // Convert config to JSON string + configJSON, err := json.Marshal(middleware.Config) + if err != nil { + log.Printf("Error encoding config: %v", err) + ResponseWithError(c, http.StatusInternalServerError, "Failed to encode config") + return + } + + // Insert into database using a transaction + tx, err := h.DB.Begin() + if err != nil { + log.Printf("Error beginning transaction: %v", err) + ResponseWithError(c, http.StatusInternalServerError, "Database error") + return + } + + // If something goes wrong, rollback + var txErr error + defer func() { + if txErr != nil { + tx.Rollback() + log.Printf("Transaction rolled back due to error: %v", txErr) + } + }() + + log.Printf("Attempting to insert middleware with ID=%s, name=%s, type=%s", + id, middleware.Name, middleware.Type) + + result, txErr := tx.Exec( + "INSERT INTO middlewares (id, name, type, config) VALUES (?, ?, ?, ?)", + id, middleware.Name, middleware.Type, string(configJSON), + ) + + if txErr != nil { + log.Printf("Error inserting middleware: %v", txErr) + ResponseWithError(c, http.StatusInternalServerError, "Failed to save middleware") + return + } + + rowsAffected, err := result.RowsAffected() + if err == nil { + log.Printf("Insert affected %d rows", rowsAffected) + } + + // Commit the transaction + if txErr = tx.Commit(); txErr != nil { + log.Printf("Error committing transaction: %v", txErr) + ResponseWithError(c, http.StatusInternalServerError, "Database error") + return + } + + log.Printf("Successfully created middleware %s (%s)", middleware.Name, id) + c.JSON(http.StatusCreated, gin.H{ + "id": id, + "name": middleware.Name, + "type": middleware.Type, + "config": middleware.Config, + }) +} + +// GetMiddleware returns a specific middleware configuration +func (h *MiddlewareHandler) GetMiddleware(c *gin.Context) { + id := c.Param("id") + if id == "" { + ResponseWithError(c, http.StatusBadRequest, "Middleware ID is required") + return + } + + var name, typ, configStr string + err := h.DB.QueryRow("SELECT name, type, config FROM middlewares WHERE id = ?", id).Scan(&name, &typ, &configStr) + if err == sql.ErrNoRows { + ResponseWithError(c, http.StatusNotFound, "Middleware not found") + return + } else if err != nil { + log.Printf("Error fetching middleware: %v", err) + ResponseWithError(c, http.StatusInternalServerError, "Failed to fetch middleware") + return + } + + var config map[string]interface{} + if err := json.Unmarshal([]byte(configStr), &config); err != nil { + log.Printf("Error parsing middleware config: %v", err) + config = map[string]interface{}{} + } + + c.JSON(http.StatusOK, gin.H{ + "id": id, + "name": name, + "type": typ, + "config": config, + }) +} + +// UpdateMiddleware updates a middleware configuration +func (h *MiddlewareHandler) UpdateMiddleware(c *gin.Context) { + id := c.Param("id") + if id == "" { + ResponseWithError(c, http.StatusBadRequest, "Middleware ID is required") + return + } + + var middleware struct { + Name string `json:"name" binding:"required"` + Type string `json:"type" binding:"required"` + Config map[string]interface{} `json:"config" binding:"required"` + } + + if err := c.ShouldBindJSON(&middleware); err != nil { + ResponseWithError(c, http.StatusBadRequest, fmt.Sprintf("Invalid request: %v", err)) + return + } + + // Validate middleware type + if !isValidMiddlewareType(middleware.Type) { + ResponseWithError(c, http.StatusBadRequest, fmt.Sprintf("Invalid middleware type: %s", middleware.Type)) + return + } + + // Check if middleware exists + var exists int + err := h.DB.QueryRow("SELECT 1 FROM middlewares WHERE id = ?", id).Scan(&exists) + if err == sql.ErrNoRows { + ResponseWithError(c, http.StatusNotFound, "Middleware not found") + return + } else if err != nil { + log.Printf("Error checking middleware existence: %v", err) + ResponseWithError(c, http.StatusInternalServerError, "Database error") + return + } + + // Convert config to JSON string + configJSON, err := json.Marshal(middleware.Config) + if err != nil { + log.Printf("Error encoding config: %v", err) + ResponseWithError(c, http.StatusInternalServerError, "Failed to encode config") + return + } + + // Update in database using a transaction + tx, err := h.DB.Begin() + if err != nil { + log.Printf("Error beginning transaction: %v", err) + ResponseWithError(c, http.StatusInternalServerError, "Database error") + return + } + + // If something goes wrong, rollback + var txErr error + defer func() { + if txErr != nil { + tx.Rollback() + log.Printf("Transaction rolled back due to error: %v", txErr) + } + }() + + log.Printf("Attempting to update middleware %s with name=%s, type=%s", + id, middleware.Name, middleware.Type) + + result, txErr := tx.Exec( + "UPDATE middlewares SET name = ?, type = ?, config = ?, updated_at = ? WHERE id = ?", + middleware.Name, middleware.Type, string(configJSON), time.Now(), id, + ) + + if txErr != nil { + log.Printf("Error updating middleware: %v", txErr) + ResponseWithError(c, http.StatusInternalServerError, "Failed to update middleware") + return + } + + rowsAffected, err := result.RowsAffected() + if err == nil { + log.Printf("Update affected %d rows", rowsAffected) + if rowsAffected == 0 { + log.Printf("Warning: Update query succeeded but no rows were affected") + } + } + + // Commit the transaction + if txErr = tx.Commit(); txErr != nil { + log.Printf("Error committing transaction: %v", txErr) + ResponseWithError(c, http.StatusInternalServerError, "Database error") + return + } + + // Double-check that the middleware was updated + var updatedName string + err = h.DB.QueryRow("SELECT name FROM middlewares WHERE id = ?", id).Scan(&updatedName) + if err != nil { + log.Printf("Warning: Could not verify middleware update: %v", err) + } else if updatedName != middleware.Name { + log.Printf("Warning: Name mismatch after update. Expected '%s', got '%s'", middleware.Name, updatedName) + } else { + log.Printf("Successfully verified middleware update for %s", id) + } + + // Return the updated middleware + c.JSON(http.StatusOK, gin.H{ + "id": id, + "name": middleware.Name, + "type": middleware.Type, + "config": middleware.Config, + }) +} + +// DeleteMiddleware deletes a middleware configuration +func (h *MiddlewareHandler) DeleteMiddleware(c *gin.Context) { + id := c.Param("id") + if id == "" { + ResponseWithError(c, http.StatusBadRequest, "Middleware ID is required") + return + } + + // Check for dependencies first + var count int + err := h.DB.QueryRow("SELECT COUNT(*) FROM resource_middlewares WHERE middleware_id = ?", id).Scan(&count) + if err != nil { + log.Printf("Error checking middleware dependencies: %v", err) + ResponseWithError(c, http.StatusInternalServerError, "Database error") + return + } + + if count > 0 { + ResponseWithError(c, http.StatusConflict, fmt.Sprintf("Cannot delete middleware because it is used by %d resources", count)) + return + } + + // Delete from database using a transaction + tx, err := h.DB.Begin() + if err != nil { + log.Printf("Error beginning transaction: %v", err) + ResponseWithError(c, http.StatusInternalServerError, "Database error") + return + } + + // If something goes wrong, rollback + var txErr error + defer func() { + if txErr != nil { + tx.Rollback() + log.Printf("Transaction rolled back due to error: %v", txErr) + } + }() + + log.Printf("Attempting to delete middleware %s", id) + + result, txErr := tx.Exec("DELETE FROM middlewares WHERE id = ?", id) + if txErr != nil { + log.Printf("Error deleting middleware: %v", txErr) + ResponseWithError(c, http.StatusInternalServerError, "Failed to delete middleware") + return + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + log.Printf("Error getting rows affected: %v", err) + ResponseWithError(c, http.StatusInternalServerError, "Database error") + return + } + + if rowsAffected == 0 { + ResponseWithError(c, http.StatusNotFound, "Middleware not found") + return + } + + log.Printf("Delete affected %d rows", rowsAffected) + + // Commit the transaction + if txErr = tx.Commit(); txErr != nil { + log.Printf("Error committing transaction: %v", txErr) + ResponseWithError(c, http.StatusInternalServerError, "Database error") + return + } + + log.Printf("Successfully deleted middleware %s", id) + c.JSON(http.StatusOK, gin.H{"message": "Middleware deleted successfully"}) +} \ No newline at end of file diff --git a/api/handlers/resources.go b/api/handlers/resources.go new file mode 100644 index 0000000..06beed0 --- /dev/null +++ b/api/handlers/resources.go @@ -0,0 +1,561 @@ +package handlers + +import ( + "database/sql" + "fmt" + "log" + "net/http" + + "github.com/gin-gonic/gin" +) + +// ResourceHandler handles resource-related requests +type ResourceHandler struct { + DB *sql.DB +} + +// NewResourceHandler creates a new resource handler +func NewResourceHandler(db *sql.DB) *ResourceHandler { + return &ResourceHandler{DB: db} +} + +// GetResources returns all resources and their assigned middlewares +func (h *ResourceHandler) GetResources(c *gin.Context) { + rows, err := h.DB.Query(` + SELECT r.id, r.host, r.service_id, r.org_id, r.site_id, r.status, + r.entrypoints, r.tls_domains, r.tcp_enabled, r.tcp_entrypoints, r.tcp_sni_rule, + r.custom_headers, r.router_priority, + GROUP_CONCAT(m.id || ':' || m.name || ':' || rm.priority, ',') as middlewares + FROM resources r + LEFT JOIN resource_middlewares rm ON r.id = rm.resource_id + LEFT JOIN middlewares m ON rm.middleware_id = m.id + GROUP BY r.id + `) + if err != nil { + log.Printf("Error fetching resources: %v", err) + ResponseWithError(c, http.StatusInternalServerError, "Failed to fetch resources") + return + } + defer rows.Close() + + var resources []map[string]interface{} + for rows.Next() { + var id, host, serviceID, orgID, siteID, status, entrypoints, tlsDomains, tcpEntrypoints, tcpSNIRule, customHeaders string + var tcpEnabled int + var routerPriority sql.NullInt64 + var middlewares sql.NullString + + if err := rows.Scan(&id, &host, &serviceID, &orgID, &siteID, &status, + &entrypoints, &tlsDomains, &tcpEnabled, &tcpEntrypoints, &tcpSNIRule, + &customHeaders, &routerPriority, &middlewares); err != nil { + log.Printf("Error scanning resource row: %v", err) + continue + } + + // Use default priority if null + priority := 100 // Default value + if routerPriority.Valid { + priority = int(routerPriority.Int64) + } + + resource := map[string]interface{}{ + "id": id, + "host": host, + "service_id": serviceID, + "org_id": orgID, + "site_id": siteID, + "status": status, + "entrypoints": entrypoints, + "tls_domains": tlsDomains, + "tcp_enabled": tcpEnabled > 0, + "tcp_entrypoints": tcpEntrypoints, + "tcp_sni_rule": tcpSNIRule, + "custom_headers": customHeaders, + "router_priority": priority, + } + + if middlewares.Valid { + resource["middlewares"] = middlewares.String + } else { + resource["middlewares"] = "" + } + + resources = append(resources, resource) + } + + if err := rows.Err(); err != nil { + log.Printf("Error during resource rows iteration: %v", err) + ResponseWithError(c, http.StatusInternalServerError, "Failed to fetch resources") + return + } + + c.JSON(http.StatusOK, resources) +} + +// GetResource returns a specific resource +func (h *ResourceHandler) GetResource(c *gin.Context) { + id := c.Param("id") + if id == "" { + ResponseWithError(c, http.StatusBadRequest, "Resource ID is required") + return + } + + var host, serviceID, orgID, siteID, status, entrypoints, tlsDomains, tcpEntrypoints, tcpSNIRule, customHeaders string + var tcpEnabled int + var routerPriority sql.NullInt64 + var middlewares sql.NullString + + err := h.DB.QueryRow(` + SELECT r.host, r.service_id, r.org_id, r.site_id, r.status, + r.entrypoints, r.tls_domains, r.tcp_enabled, r.tcp_entrypoints, r.tcp_sni_rule, + r.custom_headers, r.router_priority, + GROUP_CONCAT(m.id || ':' || m.name || ':' || rm.priority, ',') as middlewares + FROM resources r + LEFT JOIN resource_middlewares rm ON r.id = rm.resource_id + LEFT JOIN middlewares m ON rm.middleware_id = m.id + WHERE r.id = ? + GROUP BY r.id + `, id).Scan(&host, &serviceID, &orgID, &siteID, &status, + &entrypoints, &tlsDomains, &tcpEnabled, &tcpEntrypoints, &tcpSNIRule, + &customHeaders, &routerPriority, &middlewares) + + if err == sql.ErrNoRows { + ResponseWithError(c, http.StatusNotFound, fmt.Sprintf("Resource not found: %s", id)) + return + } else if err != nil { + log.Printf("Error fetching resource: %v", err) + ResponseWithError(c, http.StatusInternalServerError, "Failed to fetch resource") + return + } + + // Use default priority if null + priority := 100 // Default value + if routerPriority.Valid { + priority = int(routerPriority.Int64) + } + + resource := map[string]interface{}{ + "id": id, + "host": host, + "service_id": serviceID, + "org_id": orgID, + "site_id": siteID, + "status": status, + "entrypoints": entrypoints, + "tls_domains": tlsDomains, + "tcp_enabled": tcpEnabled > 0, + "tcp_entrypoints": tcpEntrypoints, + "tcp_sni_rule": tcpSNIRule, + "custom_headers": customHeaders, + "router_priority": priority, + } + + if middlewares.Valid { + resource["middlewares"] = middlewares.String + } else { + resource["middlewares"] = "" + } + + c.JSON(http.StatusOK, resource) +} + +// DeleteResource deletes a resource from the database +func (h *ResourceHandler) DeleteResource(c *gin.Context) { + id := c.Param("id") + if id == "" { + ResponseWithError(c, http.StatusBadRequest, "Resource ID is required") + return + } + + // Check if resource exists and its status + var status string + err := h.DB.QueryRow("SELECT status FROM resources WHERE id = ?", id).Scan(&status) + if err == sql.ErrNoRows { + ResponseWithError(c, http.StatusNotFound, "Resource not found") + return + } else if err != nil { + log.Printf("Error checking resource existence: %v", err) + ResponseWithError(c, http.StatusInternalServerError, "Database error") + return + } + + // Only allow deletion of disabled resources + if status != "disabled" { + ResponseWithError(c, http.StatusBadRequest, "Only disabled resources can be deleted") + return + } + + // Delete the resource using a transaction + tx, err := h.DB.Begin() + if err != nil { + log.Printf("Error beginning transaction: %v", err) + ResponseWithError(c, http.StatusInternalServerError, "Database error") + return + } + + // If something goes wrong, rollback + var txErr error + defer func() { + if txErr != nil { + tx.Rollback() + log.Printf("Transaction rolled back due to error: %v", txErr) + } + }() + + // First delete any middleware relationships + log.Printf("Removing middleware relationships for resource %s", id) + _, txErr = tx.Exec("DELETE FROM resource_middlewares WHERE resource_id = ?", id) + if txErr != nil { + log.Printf("Error removing resource middlewares: %v", txErr) + ResponseWithError(c, http.StatusInternalServerError, "Failed to delete resource") + return + } + + // Then delete the resource + log.Printf("Deleting resource %s", id) + result, txErr := tx.Exec("DELETE FROM resources WHERE id = ?", id) + if txErr != nil { + log.Printf("Error deleting resource: %v", txErr) + ResponseWithError(c, http.StatusInternalServerError, "Failed to delete resource") + return + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + log.Printf("Error getting rows affected: %v", err) + ResponseWithError(c, http.StatusInternalServerError, "Database error") + return + } + + if rowsAffected == 0 { + ResponseWithError(c, http.StatusNotFound, "Resource not found") + return + } + + log.Printf("Delete affected %d rows", rowsAffected) + + // Commit the transaction + if txErr = tx.Commit(); txErr != nil { + log.Printf("Error committing transaction: %v", txErr) + ResponseWithError(c, http.StatusInternalServerError, "Database error") + return + } + + log.Printf("Successfully deleted resource %s", id) + c.JSON(http.StatusOK, gin.H{"message": "Resource deleted successfully"}) +} + +// AssignMiddleware assigns a middleware to a resource +func (h *ResourceHandler) AssignMiddleware(c *gin.Context) { + resourceID := c.Param("id") + if resourceID == "" { + ResponseWithError(c, http.StatusBadRequest, "Resource ID is required") + return + } + + var input struct { + MiddlewareID string `json:"middleware_id" binding:"required"` + Priority int `json:"priority"` + } + + if err := c.ShouldBindJSON(&input); err != nil { + ResponseWithError(c, http.StatusBadRequest, fmt.Sprintf("Invalid request: %v", err)) + return + } + + // Default priority is 100 if not specified + if input.Priority <= 0 { + input.Priority = 100 + } + + // Verify resource exists + var exists int + var status string + err := h.DB.QueryRow("SELECT 1, status FROM resources WHERE id = ?", resourceID).Scan(&exists, &status) + if err == sql.ErrNoRows { + ResponseWithError(c, http.StatusNotFound, "Resource not found") + return + } else if err != nil { + log.Printf("Error checking resource existence: %v", err) + ResponseWithError(c, http.StatusInternalServerError, "Database error") + return + } + + // Don't allow attaching middlewares to disabled resources + if status == "disabled" { + ResponseWithError(c, http.StatusBadRequest, "Cannot assign middleware to a disabled resource") + return + } + + // Verify middleware exists + err = h.DB.QueryRow("SELECT 1 FROM middlewares WHERE id = ?", input.MiddlewareID).Scan(&exists) + if err == sql.ErrNoRows { + ResponseWithError(c, http.StatusNotFound, "Middleware not found") + return + } else if err != nil { + log.Printf("Error checking middleware existence: %v", err) + ResponseWithError(c, http.StatusInternalServerError, "Database error") + return + } + + // Insert or update the resource middleware relationship using a transaction + tx, err := h.DB.Begin() + if err != nil { + log.Printf("Error beginning transaction: %v", err) + ResponseWithError(c, http.StatusInternalServerError, "Database error") + return + } + + // If something goes wrong, rollback + var txErr error + defer func() { + if txErr != nil { + tx.Rollback() + log.Printf("Transaction rolled back due to error: %v", txErr) + } + }() + + // First delete any existing relationship + log.Printf("Removing existing middleware relationship: resource=%s, middleware=%s", + resourceID, input.MiddlewareID) + _, txErr = tx.Exec( + "DELETE FROM resource_middlewares WHERE resource_id = ? AND middleware_id = ?", + resourceID, input.MiddlewareID, + ) + if txErr != nil { + log.Printf("Error removing existing relationship: %v", txErr) + ResponseWithError(c, http.StatusInternalServerError, "Database error") + return + } + + // Then insert the new relationship + log.Printf("Creating new middleware relationship: resource=%s, middleware=%s, priority=%d", + resourceID, input.MiddlewareID, input.Priority) + result, txErr := tx.Exec( + "INSERT INTO resource_middlewares (resource_id, middleware_id, priority) VALUES (?, ?, ?)", + resourceID, input.MiddlewareID, input.Priority, + ) + if txErr != nil { + log.Printf("Error assigning middleware: %v", txErr) + ResponseWithError(c, http.StatusInternalServerError, "Failed to assign middleware") + return + } + + rowsAffected, err := result.RowsAffected() + if err == nil { + log.Printf("Insert affected %d rows", rowsAffected) + } + + // Commit the transaction + if txErr = tx.Commit(); txErr != nil { + log.Printf("Error committing transaction: %v", txErr) + ResponseWithError(c, http.StatusInternalServerError, "Database error") + return + } + + log.Printf("Successfully assigned middleware %s to resource %s with priority %d", + input.MiddlewareID, resourceID, input.Priority) + c.JSON(http.StatusOK, gin.H{ + "resource_id": resourceID, + "middleware_id": input.MiddlewareID, + "priority": input.Priority, + }) +} + +// AssignMultipleMiddlewares assigns multiple middlewares to a resource in one operation +func (h *ResourceHandler) AssignMultipleMiddlewares(c *gin.Context) { + resourceID := c.Param("id") + if resourceID == "" { + ResponseWithError(c, http.StatusBadRequest, "Resource ID is required") + return + } + + var input struct { + Middlewares []struct { + MiddlewareID string `json:"middleware_id" binding:"required"` + Priority int `json:"priority"` + } `json:"middlewares" binding:"required"` + } + + if err := c.ShouldBindJSON(&input); err != nil { + ResponseWithError(c, http.StatusBadRequest, fmt.Sprintf("Invalid request: %v", err)) + return + } + + // Verify resource exists and is active + var exists int + var status string + err := h.DB.QueryRow("SELECT 1, status FROM resources WHERE id = ?", resourceID).Scan(&exists, &status) + if err == sql.ErrNoRows { + ResponseWithError(c, http.StatusNotFound, "Resource not found") + return + } else if err != nil { + log.Printf("Error checking resource existence: %v", err) + ResponseWithError(c, http.StatusInternalServerError, "Database error") + return + } + + // Don't allow attaching middlewares to disabled resources + if status == "disabled" { + ResponseWithError(c, http.StatusBadRequest, "Cannot assign middlewares to a disabled resource") + return + } + + // Start a transaction + tx, err := h.DB.Begin() + if err != nil { + log.Printf("Error beginning transaction: %v", err) + ResponseWithError(c, http.StatusInternalServerError, "Database error") + return + } + + // If something goes wrong, rollback + var txErr error + defer func() { + if txErr != nil { + tx.Rollback() + log.Printf("Transaction rolled back due to error: %v", txErr) + } + }() + + // Process each middleware + successful := make([]map[string]interface{}, 0) + log.Printf("Assigning %d middlewares to resource %s", len(input.Middlewares), resourceID) + + for _, mw := range input.Middlewares { + // Default priority is 100 if not specified + if mw.Priority <= 0 { + mw.Priority = 100 + } + + // Verify middleware exists + var middlewareExists int + err := h.DB.QueryRow("SELECT 1 FROM middlewares WHERE id = ?", mw.MiddlewareID).Scan(&middlewareExists) + if err == sql.ErrNoRows { + // Skip this middleware but don't fail the entire request + log.Printf("Middleware %s not found, skipping", mw.MiddlewareID) + continue + } else if err != nil { + log.Printf("Error checking middleware existence: %v", err) + ResponseWithError(c, http.StatusInternalServerError, "Database error") + return + } + + // First delete any existing relationship + log.Printf("Removing existing relationship: resource=%s, middleware=%s", + resourceID, mw.MiddlewareID) + _, txErr = tx.Exec( + "DELETE FROM resource_middlewares WHERE resource_id = ? AND middleware_id = ?", + resourceID, mw.MiddlewareID, + ) + if txErr != nil { + log.Printf("Error removing existing relationship: %v", txErr) + ResponseWithError(c, http.StatusInternalServerError, "Database error") + return + } + + // Then insert the new relationship + log.Printf("Creating new relationship: resource=%s, middleware=%s, priority=%d", + resourceID, mw.MiddlewareID, mw.Priority) + result, txErr := tx.Exec( + "INSERT INTO resource_middlewares (resource_id, middleware_id, priority) VALUES (?, ?, ?)", + resourceID, mw.MiddlewareID, mw.Priority, + ) + if txErr != nil { + log.Printf("Error assigning middleware: %v", txErr) + ResponseWithError(c, http.StatusInternalServerError, "Failed to assign middleware") + return + } + + rowsAffected, err := result.RowsAffected() + if err == nil && rowsAffected > 0 { + log.Printf("Successfully assigned middleware %s with priority %d", + mw.MiddlewareID, mw.Priority) + successful = append(successful, map[string]interface{}{ + "middleware_id": mw.MiddlewareID, + "priority": mw.Priority, + }) + } else { + log.Printf("Warning: Insertion query succeeded but affected %d rows", rowsAffected) + } + } + + // Commit the transaction + if txErr = tx.Commit(); txErr != nil { + log.Printf("Error committing transaction: %v", txErr) + ResponseWithError(c, http.StatusInternalServerError, "Database error") + return + } + + log.Printf("Successfully assigned %d middlewares to resource %s", len(successful), resourceID) + c.JSON(http.StatusOK, gin.H{ + "resource_id": resourceID, + "middlewares": successful, + }) +} + +// RemoveMiddleware removes a middleware from a resource +func (h *ResourceHandler) RemoveMiddleware(c *gin.Context) { + resourceID := c.Param("id") + middlewareID := c.Param("middlewareId") + + if resourceID == "" || middlewareID == "" { + ResponseWithError(c, http.StatusBadRequest, "Resource ID and Middleware ID are required") + return + } + + log.Printf("Removing middleware %s from resource %s", middlewareID, resourceID) + + // Delete the relationship using a transaction + tx, err := h.DB.Begin() + if err != nil { + log.Printf("Error beginning transaction: %v", err) + ResponseWithError(c, http.StatusInternalServerError, "Database error") + return + } + + // If something goes wrong, rollback + var txErr error + defer func() { + if txErr != nil { + tx.Rollback() + log.Printf("Transaction rolled back due to error: %v", txErr) + } + }() + + result, txErr := tx.Exec( + "DELETE FROM resource_middlewares WHERE resource_id = ? AND middleware_id = ?", + resourceID, middlewareID, + ) + + if txErr != nil { + log.Printf("Error removing middleware: %v", txErr) + ResponseWithError(c, http.StatusInternalServerError, "Failed to remove middleware") + return + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + log.Printf("Error getting rows affected: %v", err) + ResponseWithError(c, http.StatusInternalServerError, "Database error") + return + } + + if rowsAffected == 0 { + log.Printf("No relationship found between resource %s and middleware %s", resourceID, middlewareID) + ResponseWithError(c, http.StatusNotFound, "Resource middleware relationship not found") + return + } + + log.Printf("Delete affected %d rows", rowsAffected) + + // Commit the transaction + if txErr = tx.Commit(); txErr != nil { + log.Printf("Error committing transaction: %v", txErr) + ResponseWithError(c, http.StatusInternalServerError, "Database error") + return + } + + log.Printf("Successfully removed middleware %s from resource %s", middlewareID, resourceID) + c.JSON(http.StatusOK, gin.H{"message": "Middleware removed from resource successfully"}) +} \ No newline at end of file diff --git a/api/routes.go b/api/server.go similarity index 73% rename from api/routes.go rename to api/server.go index 449cb2a..a286f00 100644 --- a/api/routes.go +++ b/api/server.go @@ -2,6 +2,7 @@ package api import ( "context" + "database/sql" "log" "net/http" "os" @@ -12,14 +13,17 @@ import ( "github.com/gin-contrib/cors" "github.com/gin-contrib/static" "github.com/gin-gonic/gin" - "github.com/hhftechnology/middleware-manager/database" + "github.com/hhftechnology/middleware-manager/api/handlers" ) // Server represents the API server type Server struct { - db *database.DB - router *gin.Engine - srv *http.Server + db *sql.DB + router *gin.Engine + srv *http.Server + middlewareHandler *handlers.MiddlewareHandler + resourceHandler *handlers.ResourceHandler + configHandler *handlers.ConfigHandler } // ServerConfig contains configuration options for the server @@ -32,7 +36,7 @@ type ServerConfig struct { } // NewServer creates a new API server -func NewServer(db *database.DB, config ServerConfig) *Server { +func NewServer(db *sql.DB, config ServerConfig) *Server { // Set gin mode based on debug flag if !config.Debug { gin.SetMode(gin.ReleaseMode) @@ -69,10 +73,18 @@ func NewServer(db *database.DB, config ServerConfig) *Server { router.Use(cors.New(corsConfig)) } + // Create request handlers + middlewareHandler := handlers.NewMiddlewareHandler(db) + resourceHandler := handlers.NewResourceHandler(db) + configHandler := handlers.NewConfigHandler(db) + // Setup server server := &Server{ - db: db, - router: router, + db: db, + router: router, + middlewareHandler: middlewareHandler, + resourceHandler: resourceHandler, + configHandler: configHandler, srv: &http.Server{ Addr: ":" + config.Port, Handler: router, @@ -102,29 +114,29 @@ func (s *Server) setupRoutes(uiPath string) { // Middleware routes middlewares := api.Group("/middlewares") { - middlewares.GET("", s.getMiddlewares) - middlewares.POST("", s.createMiddleware) - middlewares.GET("/:id", s.getMiddleware) - middlewares.PUT("/:id", s.updateMiddleware) - middlewares.DELETE("/:id", s.deleteMiddleware) + middlewares.GET("", s.middlewareHandler.GetMiddlewares) + middlewares.POST("", s.middlewareHandler.CreateMiddleware) + middlewares.GET("/:id", s.middlewareHandler.GetMiddleware) + middlewares.PUT("/:id", s.middlewareHandler.UpdateMiddleware) + middlewares.DELETE("/:id", s.middlewareHandler.DeleteMiddleware) } // Resource routes resources := api.Group("/resources") { - resources.GET("", s.getResources) - resources.GET("/:id", s.getResource) - resources.DELETE("/:id", s.deleteResource) - resources.POST("/:id/middlewares", s.assignMiddleware) - resources.POST("/:id/middlewares/bulk", s.assignMultipleMiddlewares) - resources.DELETE("/:id/middlewares/:middlewareId", s.removeMiddleware) + resources.GET("", s.resourceHandler.GetResources) + resources.GET("/:id", s.resourceHandler.GetResource) + resources.DELETE("/:id", s.resourceHandler.DeleteResource) + resources.POST("/:id/middlewares", s.resourceHandler.AssignMiddleware) + resources.POST("/:id/middlewares/bulk", s.resourceHandler.AssignMultipleMiddlewares) + resources.DELETE("/:id/middlewares/:middlewareId", s.resourceHandler.RemoveMiddleware) // Router configuration routes - resources.PUT("/:id/config/http", s.updateHTTPConfig) // HTTP entrypoints - resources.PUT("/:id/config/tls", s.updateTLSConfig) // TLS certificate domains - resources.PUT("/:id/config/tcp", s.updateTCPConfig) // TCP SNI routing - resources.PUT("/:id/config/headers", s.updateHeadersConfig) // Custom Host headers - resources.PUT("/:id/config/priority", s.updateRouterPriority) // Router priority + resources.PUT("/:id/config/http", s.configHandler.UpdateHTTPConfig) // HTTP entrypoints + resources.PUT("/:id/config/tls", s.configHandler.UpdateTLSConfig) // TLS certificate domains + resources.PUT("/:id/config/tcp", s.configHandler.UpdateTCPConfig) // TCP SNI routing + resources.PUT("/:id/config/headers", s.configHandler.UpdateHeadersConfig) // Custom Host headers + resources.PUT("/:id/config/priority", s.configHandler.UpdateRouterPriority) // Router priority } } diff --git a/database/transaction.go b/database/transaction.go new file mode 100644 index 0000000..60fbcee --- /dev/null +++ b/database/transaction.go @@ -0,0 +1,98 @@ +package database + +import ( + "database/sql" + "fmt" + "log" +) + +// TxFn represents a function that uses a transaction +type TxFn func(*sql.Tx) error + +// WithTransaction wraps a function with a transaction +func (db *DB) WithTransaction(fn TxFn) error { + tx, err := db.Begin() + if err != nil { + return fmt.Errorf("failed to begin transaction: %w", err) + } + + defer func() { + if p := recover(); p != nil { + // Ensure rollback on panic + log.Printf("Recovered from panic in transaction: %v", p) + tx.Rollback() + panic(p) // Re-throw panic after rollback + } + }() + + if err := fn(tx); err != nil { + if rbErr := tx.Rollback(); rbErr != nil { + log.Printf("Warning: Rollback failed: %v (original error: %v)", rbErr, err) + return fmt.Errorf("rollback failed: %v (original error: %w)", rbErr, err) + } + log.Printf("Transaction rolled back due to error: %v", err) + return err + } + + if err := tx.Commit(); err != nil { + log.Printf("Error committing transaction: %v", err) + return fmt.Errorf("commit failed: %w", err) + } + + return nil +} + +// QueryRow executes a query that returns a single row and scans the result into the provided destination +func (db *DB) QueryRowSafe(query string, dest interface{}, args ...interface{}) error { + row := db.QueryRow(query, args...) + if err := row.Scan(dest); err != nil { + if err == sql.ErrNoRows { + return ErrNotFound + } + return fmt.Errorf("scan failed: %w", err) + } + return nil +} + +// ExecSafe executes a statement and returns the result summary +func (db *DB) ExecSafe(query string, args ...interface{}) (sql.Result, error) { + result, err := db.Exec(query, args...) + if err != nil { + return nil, fmt.Errorf("exec failed: %w", err) + } + return result, nil +} + +// CustomError types for database operations +var ( + ErrNotFound = fmt.Errorf("record not found") + ErrDuplicate = fmt.Errorf("duplicate record") + ErrConstraint = fmt.Errorf("constraint violation") +) + +// ExecTx executes a statement within a transaction and returns the result +func ExecTx(tx *sql.Tx, query string, args ...interface{}) (sql.Result, error) { + result, err := tx.Exec(query, args...) + if err != nil { + return nil, fmt.Errorf("exec in transaction failed: %w", err) + } + return result, nil +} + +// GetRowsAffected is a helper to get rows affected from a result +func GetRowsAffected(result sql.Result) (int64, error) { + affected, err := result.RowsAffected() + if err != nil { + return 0, fmt.Errorf("failed to get rows affected: %w", err) + } + return affected, nil +} + +// GetLastInsertID is a helper to get last insert ID from a result +func GetLastInsertID(result sql.Result) (int64, error) { + id, err := result.LastInsertId() + if err != nil { + return 0, fmt.Errorf("failed to get last insert ID: %w", err) + } + return id, nil +} \ No newline at end of file diff --git a/models/middleware_types.go b/models/middleware_types.go new file mode 100644 index 0000000..41bdb9f --- /dev/null +++ b/models/middleware_types.go @@ -0,0 +1,484 @@ +package models + +import ( + "strings" +) + +// MiddlewareProcessor interface for type-specific processing +type MiddlewareProcessor interface { + Process(config map[string]interface{}) map[string]interface{} +} + +// Registry of middleware processors +var middlewareProcessors = map[string]MiddlewareProcessor{ + "headers": &HeadersProcessor{}, + "basicAuth": &AuthProcessor{}, + "forwardAuth": &AuthProcessor{}, + "digestAuth": &AuthProcessor{}, + "redirectRegex": &PathProcessor{}, + "redirectScheme": &PathProcessor{}, + "replacePath": &PathProcessor{}, + "replacePathRegex": &PathProcessor{}, + "stripPrefix": &PathProcessor{}, + "stripPrefixRegex": &PathProcessor{}, + "chain": &ChainProcessor{}, + "plugin": &PluginProcessor{}, + "rateLimit": &RateLimitProcessor{}, + "inFlightReq": &RateLimitProcessor{}, + "ipWhiteList": &IPFilterProcessor{}, + "ipAllowList": &IPFilterProcessor{}, + // Add more middleware types as needed +} + +// GetProcessor returns the appropriate processor for a middleware type +func GetProcessor(middlewareType string) MiddlewareProcessor { + if processor, exists := middlewareProcessors[middlewareType]; exists { + return processor + } + return &DefaultProcessor{} // Fallback processor +} + +// ProcessMiddlewareConfig processes a middleware configuration based on its type +func ProcessMiddlewareConfig(middlewareType string, config map[string]interface{}) map[string]interface{} { + processor := GetProcessor(middlewareType) + return processor.Process(config) +} + +// DefaultProcessor is the fallback processor for middleware types without a specific processor +type DefaultProcessor struct{} + +// Process handles general middleware configuration processing +func (p *DefaultProcessor) Process(config map[string]interface{}) map[string]interface{} { + return preserveTraefikValues(config).(map[string]interface{}) +} + +// HeadersProcessor handles headers middleware specific processing +type HeadersProcessor struct{} + +// Process implements special handling for headers middleware +func (p *HeadersProcessor) Process(config map[string]interface{}) map[string]interface{} { + // Special handling for response headers which might contain empty strings + if customResponseHeaders, ok := config["customResponseHeaders"].(map[string]interface{}); ok { + for key, value := range customResponseHeaders { + // Ensure empty strings are preserved exactly + if strValue, ok := value.(string); ok { + customResponseHeaders[key] = strValue + } + } + } + + // Special handling for request headers which might contain empty strings + if customRequestHeaders, ok := config["customRequestHeaders"].(map[string]interface{}); ok { + for key, value := range customRequestHeaders { + // Ensure empty strings are preserved exactly + if strValue, ok := value.(string); ok { + customRequestHeaders[key] = strValue + } + } + } + + // Process header fields that are often quoted strings + specialStringFields := []string{ + "customFrameOptionsValue", "contentSecurityPolicy", + "referrerPolicy", "permissionsPolicy", + } + + for _, field := range specialStringFields { + if value, ok := config[field].(string); ok { + // Preserve string exactly, especially if it contains quotes + config[field] = value + } + } + + // Process other header configuration values with general processor + return preserveTraefikValues(config).(map[string]interface{}) +} + +// AuthProcessor handles authentication middlewares specific processing +type AuthProcessor struct{} + +// Process implements special handling for authentication middlewares +func (p *AuthProcessor) Process(config map[string]interface{}) map[string]interface{} { + // ForwardAuth middleware special handling + if address, ok := config["address"].(string); ok { + // Preserve address URL exactly + config["address"] = address + } + + // Process trust settings + if trustForward, ok := config["trustForwardHeader"].(bool); ok { + config["trustForwardHeader"] = trustForward + } + + // Process headers array + if headers, ok := config["authResponseHeaders"].([]interface{}); ok { + for i, header := range headers { + if headerStr, ok := header.(string); ok { + headers[i] = headerStr + } + } + } + + // BasicAuth/DigestAuth middleware special handling + // Preserve exact format of users array + if users, ok := config["users"].([]interface{}); ok { + for i, user := range users { + if userStr, ok := user.(string); ok { + users[i] = userStr + } + } + } + + // Process other auth configuration values with general processor + return preserveTraefikValues(config).(map[string]interface{}) +} + +// PathProcessor handles path manipulation middlewares specific processing +type PathProcessor struct{} + +// Process implements special handling for path manipulation middlewares +func (p *PathProcessor) Process(config map[string]interface{}) map[string]interface{} { + // Special handling for regex patterns - these need exact preservation + if regex, ok := config["regex"].(string); ok { + // Preserve regex pattern exactly + config["regex"] = regex + } else if regexList, ok := config["regex"].([]interface{}); ok { + // Handle regex arrays (like in stripPrefixRegex) + for i, r := range regexList { + if regexStr, ok := r.(string); ok { + regexList[i] = regexStr + } + } + } + + // Special handling for replacement patterns + if replacement, ok := config["replacement"].(string); ok { + // Preserve replacement pattern exactly + config["replacement"] = replacement + } + + // Special handling for path values + if path, ok := config["path"].(string); ok { + // Preserve path exactly + config["path"] = path + } + + // Special handling for prefixes arrays + if prefixes, ok := config["prefixes"].([]interface{}); ok { + for i, prefix := range prefixes { + if prefixStr, ok := prefix.(string); ok { + prefixes[i] = prefixStr + } + } + } + + // Special handling for scheme + if scheme, ok := config["scheme"].(string); ok { + // Preserve scheme exactly + config["scheme"] = scheme + } + + // Process boolean options + if forceSlash, ok := config["forceSlash"].(bool); ok { + config["forceSlash"] = forceSlash + } + + if permanent, ok := config["permanent"].(bool); ok { + config["permanent"] = permanent + } + + // Process other path manipulation configuration values with general processor + return preserveTraefikValues(config).(map[string]interface{}) +} + +// ChainProcessor handles chain middleware specific processing +type ChainProcessor struct{} + +// Process implements special handling for chain middleware +func (p *ChainProcessor) Process(config map[string]interface{}) map[string]interface{} { + // Process middlewares array + if middlewares, ok := config["middlewares"].([]interface{}); ok { + for i, middleware := range middlewares { + if middlewareStr, ok := middleware.(string); ok { + // If this is not already a fully qualified middleware reference + if !strings.Contains(middlewareStr, "@") { + // Assume it's from our file provider + middlewares[i] = middlewareStr + } + } + } + } + + // Process other chain configuration values with general processor + return preserveTraefikValues(config).(map[string]interface{}) +} + +// PluginProcessor handles plugin middleware specific processing +type PluginProcessor struct{} + +// Process implements special handling for plugin middleware +func (p *PluginProcessor) Process(config map[string]interface{}) map[string]interface{} { + // Process plugins (including CrowdSec) + for _, pluginCfg := range config { + if pluginConfig, ok := pluginCfg.(map[string]interface{}); ok { + // Process special fields in plugin configurations + + // Process API keys and secrets - must be preserved exactly + keyFields := []string{ + "crowdsecLapiKey", "apiKey", "token", "secret", "password", + "key", "accessKey", "secretKey", "captchaSiteKey", "captchaSecretKey", + } + + for _, field := range keyFields { + if val, exists := pluginConfig[field]; exists { + if strVal, ok := val.(string); ok { + // Ensure keys are preserved exactly as-is + pluginConfig[field] = strVal + } + } + } + + // Process boolean options + boolFields := []string{ + "enabled", "failureBlock", "unreachableBlock", "insecureVerify", + "allowLocalRequests", "logLocalRequests", "logAllowedRequests", + "logApiRequests", "silentStartUp", "forceMonthlyUpdate", + "allowUnknownCountries", "blackListMode", "addCountryHeader", + } + + for _, field := range boolFields { + for configKey, val := range pluginConfig { + if strings.Contains(configKey, field) { + if boolVal, ok := val.(bool); ok { + pluginConfig[configKey] = boolVal + } + } + } + } + + // Process arrays + arrayFields := []string{ + "forwardedHeadersTrustedIPs", "clientTrustedIPs", "countries", + } + + for _, field := range arrayFields { + for configKey, val := range pluginConfig { + if strings.Contains(configKey, field) { + if arrayVal, ok := val.([]interface{}); ok { + for i, item := range arrayVal { + if strItem, ok := item.(string); ok { + arrayVal[i] = strItem + } + } + } + } + } + } + + // Process remaining fields with general processor + preserveTraefikValues(pluginConfig) + } + } + + // Process the entire config with general processor + return preserveTraefikValues(config).(map[string]interface{}) +} + +// RateLimitProcessor handles rate limiting middlewares specific processing +type RateLimitProcessor struct{} + +// Process implements special handling for rate limiting middlewares +func (p *RateLimitProcessor) Process(config map[string]interface{}) map[string]interface{} { + // Process numeric values + if average, ok := config["average"].(float64); ok { + // Convert to int if it's a whole number + if average == float64(int(average)) { + config["average"] = int(average) + } else { + config["average"] = average + } + } + + if burst, ok := config["burst"].(float64); ok { + // Convert to int if it's a whole number + if burst == float64(int(burst)) { + config["burst"] = int(burst) + } else { + config["burst"] = burst + } + } + + if amount, ok := config["amount"].(float64); ok { + // Convert to int if it's a whole number + if amount == float64(int(amount)) { + config["amount"] = int(amount) + } else { + config["amount"] = amount + } + } + + // Process sourceCriterion for inFlightReq + if sourceCriterion, ok := config["sourceCriterion"].(map[string]interface{}); ok { + // Process IP strategy + if ipStrategy, ok := sourceCriterion["ipStrategy"].(map[string]interface{}); ok { + // Process depth + if depth, ok := ipStrategy["depth"].(float64); ok { + ipStrategy["depth"] = int(depth) + } + + // Process excluded IPs + if excludedIPs, ok := ipStrategy["excludedIPs"].([]interface{}); ok { + for i, ip := range excludedIPs { + if ipStr, ok := ip.(string); ok { + excludedIPs[i] = ipStr + } + } + } + } + + // Process requestHost boolean + if requestHost, ok := sourceCriterion["requestHost"].(bool); ok { + sourceCriterion["requestHost"] = requestHost + } + } + + // Process other rate limiting configuration values with general processor + return preserveTraefikValues(config).(map[string]interface{}) +} + +// IPFilterProcessor handles IP filtering middlewares specific processing +type IPFilterProcessor struct{} + +// Process implements special handling for IP filtering middlewares +func (p *IPFilterProcessor) Process(config map[string]interface{}) map[string]interface{} { + // Process sourceRange IPs + if sourceRange, ok := config["sourceRange"].([]interface{}); ok { + for i, range_ := range sourceRange { + if rangeStr, ok := range_.(string); ok { + // Preserve IP CIDR notation exactly + sourceRange[i] = rangeStr + } + } + } + + // Process other IP filtering configuration values with general processor + return preserveTraefikValues(config).(map[string]interface{}) +} + +// preserveTraefikValues ensures all values in Traefik configurations are properly handled +// This handles special cases in different middleware types and ensures precise value preservation +func preserveTraefikValues(data interface{}) interface{} { + if data == nil { + return nil + } + + switch v := data.(type) { + case map[string]interface{}: + // Process each key-value pair in the map + for key, val := range v { + // Process values based on key names that might need special handling + switch { + // URL or path related fields + case key == "path" || key == "url" || key == "address" || strings.HasSuffix(key, "Path"): + // Ensure path strings keep their exact format + if strVal, ok := val.(string); ok && strVal != "" { + // Keep exact string formatting + v[key] = strVal + } else { + v[key] = preserveTraefikValues(val) + } + + // Regex and replacement patterns + case key == "regex" || key == "replacement" || strings.HasSuffix(key, "Regex"): + // Ensure regex patterns are preserved exactly + if strVal, ok := val.(string); ok && strVal != "" { + // Keep exact string formatting with special characters + v[key] = strVal + } else { + v[key] = preserveTraefikValues(val) + } + + // API keys and security tokens + case key == "key" || key == "token" || key == "secret" || + strings.Contains(key, "Key") || strings.Contains(key, "Token") || + strings.Contains(key, "Secret") || strings.Contains(key, "Password"): + // Ensure API keys and tokens are preserved exactly + if strVal, ok := val.(string); ok { + // Always preserve keys/tokens exactly as-is, even if empty + v[key] = strVal + } else { + v[key] = preserveTraefikValues(val) + } + + // Empty header values (common in security headers middleware) + case key == "Server" || key == "X-Powered-By" || strings.HasPrefix(key, "X-"): + // Empty string values are often used to remove headers + if strVal, ok := val.(string); ok { + // Preserve empty strings exactly + v[key] = strVal + } else { + v[key] = preserveTraefikValues(val) + } + + // IP addresses and networks + case key == "ip" || key == "clientIP" || strings.Contains(key, "IP") || + key == "sourceRange" || key == "excludedIPs": + // IP addresses often need exact formatting + v[key] = preserveTraefikValues(val) + + // Boolean flags that control behavior + case strings.HasPrefix(key, "is") || strings.HasPrefix(key, "has") || + strings.HasPrefix(key, "enable") || strings.HasSuffix(key, "enabled") || + strings.HasSuffix(key, "Enabled") || key == "permanent" || key == "forceSlash": + // Ensure boolean values are preserved as actual booleans + if boolVal, ok := val.(bool); ok { + v[key] = boolVal + } else if strVal, ok := val.(string); ok { + // Convert string "true"/"false" to actual boolean if needed + if strVal == "true" { + v[key] = true + } else if strVal == "false" { + v[key] = false + } else { + v[key] = strVal // Keep as is if not a boolean string + } + } else { + v[key] = preserveTraefikValues(val) + } + + // Integer values like timeouts, ports, limits + case key == "amount" || key == "burst" || key == "port" || + strings.HasSuffix(key, "Seconds") || strings.HasSuffix(key, "Limit") || + strings.HasSuffix(key, "Timeout") || strings.HasSuffix(key, "Size") || + key == "depth" || key == "priority" || key == "statusCode" || + key == "attempts" || key == "responseCode": + // Handle float64 to int conversion for whole numbers, common in JSON unmarshaling + if f, ok := val.(float64); ok && f == float64(int(f)) { + v[key] = int(f) + } else { + v[key] = preserveTraefikValues(val) + } + + // Default handling for other keys + default: + v[key] = preserveTraefikValues(val) + } + } + return v + + case []interface{}: + // Process each element in the array + for i, item := range v { + v[i] = preserveTraefikValues(item) + } + return v + + case string, int, float64, bool: + // Preserve primitive types as they are + return v + + default: + // For any other type, return as is + return v + } +} \ No newline at end of file diff --git a/ui/src/App.js b/ui/src/App.js index b27d2a0..0666f4e 100644 --- a/ui/src/App.js +++ b/ui/src/App.js @@ -1,260 +1,24 @@ -import React, { useState, useEffect } from 'react'; +import React from 'react'; +import { AppProvider, useApp } from './contexts/AppContext'; +import { ResourceProvider } from './contexts/ResourceContext'; +import { MiddlewareProvider } from './contexts/MiddlewareContext'; +import { Header } from './components/common'; -// Dark mode icons as inline SVG -const MoonIcon = () => ( - - - -); +// Import page components (these would be created as separate files) +import Dashboard from './components/dashboard/Dashboard'; +import ResourcesList from './components/resources/ResourcesList'; +import ResourceDetail from './components/resources/ResourceDetail'; +import MiddlewaresList from './components/middlewares/MiddlewaresList'; +import MiddlewareForm from './components/middlewares/MiddlewareForm'; -const SunIcon = () => ( - - - -); +/** + * Main application component that renders the current page + * based on the navigation state + */ +const MainContent = () => { + const { page, resourceId, middlewareId, isEditing, navigateTo } = useApp(); -// Dark mode initializer -const initializeDarkMode = () => { - // Check for saved preference or use system preference - const savedTheme = localStorage.getItem('theme'); - const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; - - if (savedTheme === 'dark' || (!savedTheme && prefersDark)) { - document.documentElement.classList.add('dark-mode'); - return true; - } - - return false; -}; - -// Dark mode toggle functionality -const toggleDarkMode = (isDark, setIsDark) => { - if (isDark) { - document.documentElement.classList.remove('dark-mode'); - localStorage.setItem('theme', 'light'); - setIsDark(false); - } else { - document.documentElement.classList.add('dark-mode'); - localStorage.setItem('theme', 'dark'); - setIsDark(true); - } -}; - -// Dark mode toggle component -const DarkModeToggle = ({ isDark, setIsDark }) => { - return ( - - ); -}; - -// --- API Service Configuration --- -const API_URL = '/api'; - -// API service object containing all endpoint calls -const api = { - // Resource-related API calls - getResources: () => - fetch(`${API_URL}/resources`).then((res) => res.json()), - getResource: (id) => - fetch(`${API_URL}/resources/${id}`).then((res) => res.json()), - deleteResource: (id) => - fetch(`${API_URL}/resources/${id}`, { - method: 'DELETE', - }).then((res) => res.json()), - assignMiddleware: (resourceId, data) => - fetch(`${API_URL}/resources/${resourceId}/middlewares`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data), - }).then((res) => res.json()), - assignMultipleMiddlewares: (resourceId, data) => - fetch(`${API_URL}/resources/${resourceId}/middlewares/bulk`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data), - }).then((res) => res.json()), - removeMiddleware: (resourceId, middlewareId) => - fetch(`${API_URL}/resources/${resourceId}/middlewares/${middlewareId}`, { - method: 'DELETE', - }).then((res) => res.json()), - - // HTTP entrypoints config -updateHTTPConfig: (resourceId, data) => - fetch(`${API_URL}/resources/${resourceId}/config/http`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data), - }).then((res) => res.json()), - -// TLS domains config -updateTLSConfig: (resourceId, data) => - fetch(`${API_URL}/resources/${resourceId}/config/tls`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data), - }).then((res) => res.json()), - -// TCP SNI config -updateTCPConfig: (resourceId, data) => - fetch(`${API_URL}/resources/${resourceId}/config/tcp`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data), - }).then((res) => res.json()), - - // Headers config - updateHeadersConfig: (resourceId, data) => - fetch(`${API_URL}/resources/${resourceId}/config/headers`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data), - }).then((res) => res.json()), - // Add to API service object - updateRouterPriority: (resourceId, data) => - fetch(`${API_URL}/resources/${resourceId}/config/priority`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data), - }).then((res) => res.json()), - // Middleware-related API calls - getMiddlewares: () => - fetch(`${API_URL}/middlewares`).then((res) => res.json()), - getMiddleware: (id) => - fetch(`${API_URL}/middlewares/${id}`).then((res) => res.json()), - createMiddleware: (data) => - fetch(`${API_URL}/middlewares`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data), - }).then((res) => res.json()), - updateMiddleware: (id, data) => - fetch(`${API_URL}/middlewares/${id}`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data), - }).then((res) => res.json()), - deleteMiddleware: (id) => - fetch(`${API_URL}/middlewares/${id}`, { - method: 'DELETE', - }).then((res) => res.json()), -}; - -// --- Helper Functions --- - -// Parses middleware string into an array of middleware objects -const parseMiddlewares = (middlewaresStr) => { - // Handle empty or invalid input - if (!middlewaresStr || typeof middlewaresStr !== 'string') return []; - - return middlewaresStr - .split(',') - .filter(Boolean) - .map((item) => { - const [id, name, priority] = item.split(':'); - return { - id, - name, - priority: parseInt(priority, 10) || 100, // Default priority if invalid - }; - }); -}; - -// Formats middleware display, including chain middleware details -const formatMiddlewareDisplay = (middleware, allMiddlewares) => { - // Determine if middleware is a chain type - const isChain = middleware.type === 'chain'; - - // Parse config safely - let configObj = middleware.config; - if (typeof configObj === 'string') { - try { - configObj = JSON.parse(configObj); - } catch (error) { - console.error('Error parsing middleware config:', error); - configObj = {}; - } - } - - return ( -
-
- {middleware.name} - - {middleware.type} - - {isChain && ( - - (Middleware Chain) - - )} -
- {/* Display chained middlewares for chain type */} - {isChain && configObj.middlewares && configObj.middlewares.length > 0 && ( -
-
Chain contains:
- -
- )} -
- ); -}; - -// --- Main App Component --- -const App = () => { - const [page, setPage] = useState('dashboard'); - const [resourceId, setResourceId] = useState(null); - const [middlewareId, setMiddlewareId] = useState(null); - const [isEditing, setIsEditing] = useState(false); - const [isDarkMode, setIsDarkMode] = useState(false); - - // Initialize dark mode on component mount - useEffect(() => { - setIsDarkMode(initializeDarkMode()); - }, []); - - // Handles navigation between pages - const navigateTo = (pageId, id = null) => { - setPage(pageId); - if (pageId === 'resource-detail') { - setResourceId(id); - } else if (pageId === 'middleware-form') { - setMiddlewareId(id); - setIsEditing(!!id); - } - }; - - // Renders the active page based on state + // Render the active page based on state const renderPage = () => { switch (page) { case 'dashboard': @@ -262,9 +26,7 @@ const App = () => { case 'resources': return ; case 'resource-detail': - return ( - - ); + return ; case 'middlewares': return ; case 'middleware-form': @@ -282,48 +44,10 @@ const App = () => { return (
- +
{renderPage()}
@@ -331,2218 +55,18 @@ const App = () => { ); }; -// --- Dashboard Component --- -const Dashboard = ({ navigateTo }) => { - const [resources, setResources] = useState([]); - const [middlewares, setMiddlewares] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [retryCount, setRetryCount] = useState(0); - const [initializationPhase, setInitializationPhase] = useState('Starting system...'); - const maxRetries = 10; // Maximum number of retry attempts - const retryDelay = 3000; // 3 seconds between retries - - // Fetch initial dashboard data - useEffect(() => { - const fetchData = async () => { - try { - setLoading(true); - - // Update initialization phase message based on retry count - if (retryCount === 1) setInitializationPhase('Connecting to database...'); - if (retryCount === 2) setInitializationPhase('Checking for resources...'); - if (retryCount === 3) setInitializationPhase('Generating configurations...'); - if (retryCount > 3) setInitializationPhase('Waiting for background services to complete...'); - - const [resourcesData, middlewaresData] = await Promise.all([ - api.getResources(), - api.getMiddlewares(), - ]); - - // Check if we got meaningful data - const hasResources = resourcesData && Array.isArray(resourcesData) && resourcesData.length > 0; - const hasMiddlewares = middlewaresData && Array.isArray(middlewaresData) && middlewaresData.length > 0; - - // If we have no data and haven't exceeded max retries, retry after delay - if ((!hasResources && !hasMiddlewares) && retryCount < maxRetries) { - setRetryCount(retryCount + 1); - setTimeout(() => fetchData(), retryDelay); - return; - } - - setResources(resourcesData || []); - setMiddlewares(middlewaresData || []); - setError(null); - setLoading(false); - } catch (err) { - console.error('Dashboard fetch error:', err); - - // If API call failed but we haven't exceeded max retries, retry after delay - if (retryCount < maxRetries) { - setRetryCount(retryCount + 1); - setInitializationPhase(`Connection attempt ${retryCount + 1}/${maxRetries} failed. Retrying...`); - setTimeout(() => fetchData(), retryDelay); - } else { - setError('Failed to load dashboard data after multiple attempts. The server might be unavailable.'); - setLoading(false); - } - } - }; - - fetchData(); - }, []); - - if (loading) { - return ( -
-
-

Initializing Middleware Manager

-

{initializationPhase}

- {retryCount > 0 && ( -
-

Attempt {retryCount} of {maxRetries}

-

The initial startup may take a moment while background services initialize.

-
- )} -
- ); - } - - if (error) { - return ( -
-

Error Loading Dashboard

-

{error}

- -
- ); - } - - // Calculate statistics for dashboard display - const protectedResources = resources.filter( - (r) => r.status !== 'disabled' && r.middlewares && r.middlewares.length > 0 - ).length; - const activeResources = resources.filter( - (r) => r.status !== 'disabled' - ).length; - const disabledResources = resources.filter( - (r) => r.status === 'disabled' - ).length; - const unprotectedResources = activeResources - protectedResources; - +/** + * Application root component with all providers + */ +const App = () => { return ( -
-

Dashboard

- - {/* Stats Section */} -
-
-

Resources

-

{activeResources}

- {disabledResources > 0 && ( -

- {disabledResources} disabled resources -

- )} -
-
-

Middlewares

-

{middlewares.length}

-
-
-

- Protected Resources -

-

- {protectedResources} / {activeResources} -

-
-
- - {/* Recent Resources Section */} -
-
-

Recent Resources

- -
-
- - - - - - - - - - - {resources.slice(0, 5).map((resource) => { - const middlewaresList = parseMiddlewares(resource.middlewares); - const isProtected = middlewaresList.length > 0; - const isDisabled = resource.status === 'disabled'; - - return ( - - - - - - - ); - })} - {resources.length === 0 && ( - - - - )} - -
- Host - - Status - - Middlewares - - Actions -
- {resource.host} - {isDisabled && ( - - Removed from Pangolin - - )} - - - {isDisabled - ? 'Disabled' - : isProtected - ? 'Protected' - : 'Not Protected'} - - - {middlewaresList.length > 0 - ? middlewaresList.length - : 'None'} - - - {isDisabled && ( - - )} -
- No resources found -
-
-
- - {/* Warning for Unprotected Resources */} - {unprotectedResources > 0 && ( -
-
-
- - - -
-
-

- You have {unprotectedResources} active resources that are not - protected with any middleware. -

-
-
-
- )} - - {/* Warning for Disabled Resources */} - {disabledResources > 0 && ( -
-
-
- - - -
-
-

- You have {disabledResources} disabled resources that were removed - from Pangolin.{' '} - navigateTo('resources')} - > - View all resources - {' '} - to delete them. -

-
-
-
- )} -
- ); -}; - -// --- Resources List Component --- -const ResourcesList = ({ navigateTo }) => { - const [resources, setResources] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [searchTerm, setSearchTerm] = useState(''); - - // Fetch all resources - const fetchResources = async () => { - try { - setLoading(true); - const data = await api.getResources(); - setResources(data); - setError(null); - } catch (err) { - setError('Failed to load resources'); - console.error('Resources fetch error:', err); - } finally { - setLoading(false); - } - }; - - useEffect(() => { - fetchResources(); - }, []); - - // Handle resource deletion with confirmation - const handleDeleteResource = async (id, host) => { - if ( - !window.confirm( - `Are you sure you want to delete the resource "${host}"? This cannot be undone.` - ) - ) { - return; - } - - try { - await api.deleteResource(id); - alert('Resource deleted successfully'); - fetchResources(); - } catch (err) { - alert( - `Failed to delete resource: ${err.message || 'Unknown error'}` - ); - console.error('Delete resource error:', err); - } - }; - - // Filter resources based on search term - const filteredResources = resources.filter((resource) => - resource.host.toLowerCase().includes(searchTerm.toLowerCase()) - ); - - return ( -
-

Resources

-
-
- setSearchTerm(e.target.value)} - /> -
- -
- {loading && !resources.length ? ( -
Loading...
- ) : error ? ( -
{error}
- ) : ( -
- - - - - - - - - - - {filteredResources.map((resource) => { - const middlewaresList = parseMiddlewares(resource.middlewares); - const isProtected = middlewaresList.length > 0; - const isDisabled = resource.status === 'disabled'; - - return ( - - - - - - - ); - })} - {filteredResources.length === 0 && ( - - - - )} - -
- Host - - Status - - Middlewares - - Actions -
- {resource.host} - {isDisabled && ( - - Removed from Pangolin - - )} - - - {isDisabled - ? 'Disabled' - : isProtected - ? 'Protected' - : 'Not Protected'} - - - {middlewaresList.length > 0 - ? middlewaresList.length - : 'None'} - - - {isDisabled && ( - - )} -
- No resources found -
-
- )} -
- ); -}; - -// --- Resource Detail Component --- -const ResourceDetail = ({ id, navigateTo }) => { - const [resource, setResource] = useState(null); - const [middlewares, setMiddlewares] = useState([]); - const [assignedMiddlewares, setAssignedMiddlewares] = useState([]); - const [availableMiddlewares, setAvailableMiddlewares] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [showModal, setShowModal] = useState(false); - const [selectedMiddlewares, setSelectedMiddlewares] = useState([]); - const [priority, setPriority] = useState(100); - // HTTP Router Configuration - const [entrypoints, setEntrypoints] = useState('websecure'); - const [showHTTPConfigModal, setShowHTTPConfigModal] = useState(false); - - // TLS Certificate Domains Configuration - const [tlsDomains, setTLSDomains] = useState(''); - const [showTLSConfigModal, setShowTLSConfigModal] = useState(false); - - // TCP SNI Router Configuration - const [tcpEnabled, setTCPEnabled] = useState(false); - const [tcpEntrypoints, setTCPEntrypoints] = useState('tcp'); - const [tcpSNIRule, setTCPSNIRule] = useState(''); - const [showTCPConfigModal, setShowTCPConfigModal] = useState(false); - - // new state variables for headers - const [customHeaders, setCustomHeaders] = useState({}); - const [showHeadersConfigModal, setShowHeadersConfigModal] = useState(false); - const [headerKey, setHeaderKey] = useState(''); - const [headerValue, setHeaderValue] = useState(''); - const [routerPriority, setRouterPriority] = useState(100); - - // Add this useEffect to load the router priority when the resource data loads - useEffect(() => { - if (resource) { - setRouterPriority(resource.router_priority || 100); - } - }, [resource]); - - // Add this handler function - const handleUpdateRouterPriority = async () => { - try { - await api.updateRouterPriority(id, { router_priority: routerPriority }); - alert('Router priority updated successfully'); - fetchData(); // Refresh data - } catch (err) { - alert('Failed to update router priority'); - console.error('Update router priority error:', err); - } - }; - - // Fetch resource and middleware data - const fetchData = async () => { - try { - setLoading(true); - const [resourceData, middlewaresData] = await Promise.all([ - api.getResource(id), - api.getMiddlewares(), - ]); - - setResource(resourceData); - setMiddlewares(middlewaresData); - // Set HTTP config - setEntrypoints(resourceData.entrypoints || 'websecure'); - // Set TLS domains config - setTLSDomains(resourceData.tls_domains || ''); - // Set TCP config - setTCPEnabled(resourceData.tcp_enabled || false); - setTCPEntrypoints(resourceData.tcp_entrypoints || 'tcp'); - setTCPSNIRule(resourceData.tcp_sni_rule || ''); - - // Add this code here for custom headers parsing - if (resourceData.custom_headers) { - try { - setCustomHeaders(JSON.parse(resourceData.custom_headers)); - } catch (e) { - console.error("Error parsing custom headers:", e); - setCustomHeaders({}); - } - } else { - setCustomHeaders({}); - } - - // Parse assigned middlewares - const middlewaresList = parseMiddlewares(resourceData.middlewares); - setAssignedMiddlewares(middlewaresList); - - // Filter out already assigned middlewares - const assignedIds = middlewaresList.map((m) => m.id); - setAvailableMiddlewares( - middlewaresData.filter((m) => !assignedIds.includes(m.id)) - ); - - setError(null); - } catch (err) { - setError('Failed to load resource details'); - console.error('Resource detail fetch error:', err); - } finally { - setLoading(false); - } - }; - - useEffect(() => { - fetchData(); - }, [id]); - const handleUpdateHTTPConfig = async (e) => { - e.preventDefault(); - - try { - await api.updateHTTPConfig(id, { entrypoints }); - setShowHTTPConfigModal(false); - fetchData(); - alert('HTTP router configuration updated successfully'); - } catch (err) { - alert('Failed to update HTTP router configuration'); - console.error('Update HTTP config error:', err); - } - }; - - const handleUpdateTLSConfig = async (e) => { - e.preventDefault(); - - try { - await api.updateTLSConfig(id, { tls_domains: tlsDomains }); - setShowTLSConfigModal(false); - fetchData(); - alert('TLS certificate domains updated successfully'); - } catch (err) { - alert('Failed to update TLS certificate domains'); - console.error('Update TLS config error:', err); - } - try { - await api.updateHeadersConfig(id, { custom_headers: customHeaders }); - setShowHeadersConfigModal(false); - fetchData(); - alert('Custom headers updated successfully'); - } catch (err) { - alert('Failed to update custom headers'); - console.error('Update headers config error:', err); - } - }; - - const handleUpdateTCPConfig = async (e) => { - e.preventDefault(); - - try { - await api.updateTCPConfig(id, { - tcp_enabled: tcpEnabled, - tcp_entrypoints: tcpEntrypoints, - tcp_sni_rule: tcpSNIRule - }); - setShowTCPConfigModal(false); - fetchData(); - alert('TCP SNI router configuration updated successfully'); - } catch (err) { - alert('Failed to update TCP SNI router configuration'); - console.error('Update TCP config error:', err); - } - }; - const handleUpdateHeadersConfig = async (e) => { - e.preventDefault(); - - try { - await api.updateHeadersConfig(id, { custom_headers: customHeaders }); - setShowHeadersConfigModal(false); - fetchData(); - alert('Custom headers updated successfully'); - } catch (err) { - alert('Failed to update custom headers'); - console.error('Update headers config error:', err); - } - }; - // Function to add new header -const addHeader = () => { - if (!headerKey.trim()) { - alert('Header key cannot be empty'); - return; - } - - setCustomHeaders({ - ...customHeaders, - [headerKey]: headerValue - }); - - setHeaderKey(''); - setHeaderValue(''); -}; - -// Function to remove header -const removeHeader = (key) => { - const newHeaders = {...customHeaders}; - delete newHeaders[key]; - setCustomHeaders(newHeaders); -}; - - // Handle multiple middleware selection - const handleMiddlewareSelection = (e) => { - const options = e.target.options; - const selected = Array.from(options) - .filter((option) => option.selected) - .map((option) => option.value); - setSelectedMiddlewares(selected); - }; - - // Assign selected middlewares to resource - const handleAssignMiddleware = async (e) => { - e.preventDefault(); - if (selectedMiddlewares.length === 0) { - alert('Please select at least one middleware'); - return; - } - - try { - const middlewaresToAdd = selectedMiddlewares.map((middlewareId) => ({ - middleware_id: middlewareId, - priority: parseInt(priority, 10), - })); - - await api.assignMultipleMiddlewares(id, { - middlewares: middlewaresToAdd, - }); - - setShowModal(false); - setSelectedMiddlewares([]); - setPriority(100); - fetchData(); - } catch (err) { - alert('Failed to assign middlewares'); - console.error('Assign middleware error:', err); - } - }; - - // Remove a middleware from resource - const handleRemoveMiddleware = async (middlewareId) => { - if ( - !window.confirm('Are you sure you want to remove this middleware?') - ) - return; - - try { - await api.removeMiddleware(id, middlewareId); - fetchData(); - } catch (err) { - alert('Failed to remove middleware'); - console.error('Remove middleware error:', err); - } - }; - - if (loading) { - return
Loading...
; - } - - if (error) { - return ( -
- {error} - -
- ); - } - - if (!resource) { - return ( -
- Resource not found - -
- ); - } - - const isDisabled = resource.status === 'disabled'; - return ( -
-
- -

Resource: {resource.host}

- {isDisabled && ( - - Removed from Pangolin - - )} -
- - {/* Disabled Resource Warning */} - {isDisabled && ( -
-
-
- - - -
-
-

- This resource has been removed from Pangolin and is now disabled. Any changes to middleware will not take effect. -

-
- - -
-
-
-
- )} - - {/* Resource Details */} -
-

Resource Details

-
-
-

Host

-

- {resource.host} - - Visit - -

-
-
-

Service ID

-

{resource.service_id}

-
-
-

Status

-

- 0 - ? 'bg-green-100 text-green-800' - : 'bg-yellow-100 text-yellow-800' - }`} - > - {isDisabled - ? 'Disabled' - : assignedMiddlewares.length > 0 - ? 'Protected' - : 'Not Protected'} - -

-
-
-

Resource ID

-

{resource.id}

-
-
-
- - {/* Router Configuration Section */} -
-

Router Configuration

-
- - - - - {/* Add the Custom Headers button here */} - -
- - {/* Current Configuration Summary */} -
-

Current Configuration

-
-
-

HTTP Entrypoints

-

{entrypoints || 'websecure'}

-
-
-

TLS Certificate Domains

-

{tlsDomains || 'None'}

-
-
-

TCP SNI Routing

-

{tcpEnabled ? 'Enabled' : 'Disabled'}

-
- {tcpEnabled && ( - <> -
-

TCP Entrypoints

-

{tcpEntrypoints || 'tcp'}

-
- {tcpSNIRule && ( -
-

TCP SNI Rule

-

{tcpSNIRule}

-
- )} - - )} - {/* Add Custom Headers summary here */} - {Object.keys(customHeaders).length > 0 && ( -
-

Custom Headers

-
- {Object.entries(customHeaders).map(([key, value]) => ( -
- {key}: {value} -
- ))} -
-
- )} -
-
-
- - {/* Add the Router Priority Configuration here */} -
-

Router Priority

-
-

- Set the priority of this router. When multiple routers match the same request, - the router with the highest priority (highest number) will be selected first. -

-

- Note: This is different from middleware priority, which controls the order middlewares - are applied within a router. -

-
- -
- setRouterPriority(parseInt(e.target.value) || 100)} - className="w-24 px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500" - min="1" - max="1000" - /> - -
-
- - {/* Middlewares Section */} -
-
-

Attached Middlewares

- -
- {assignedMiddlewares.length === 0 ? ( -
-

This resource does not have any middlewares applied to it.

-

Add a middleware to enhance security or modify behavior.

-
- ) : ( - - - - - - - - - - {assignedMiddlewares.map((middleware) => { - const middlewareDetails = - middlewares.find((m) => m.id === middleware.id) || { - id: middleware.id, - name: middleware.name, - type: 'unknown', - }; - - return ( - - - - - - ); - })} - -
- Middleware - - Priority - - Actions -
- {formatMiddlewareDisplay(middlewareDetails, middlewares)} - - {middleware.priority} - - -
- )} -
- - {/* Add Middleware Modal */} - {showModal && ( -
-
-
-

- Add Middlewares to {resource.host} -

- -
-
- {availableMiddlewares.length === 0 ? ( -
-

All middlewares have been assigned to this resource.

- -
- ) : ( -
-
- - -

- Hold Ctrl (or Cmd) to select multiple middlewares. All selected middlewares will be assigned with the same priority. -

-
-
- - setPriority(e.target.value)} - className="w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500" - min="1" - max="1000" - required - /> -

- Higher priority middlewares are applied first (1-1000) -

-
-
- - -
-
- )} -
-
-
- )} - - {/* HTTP Config Modal */} - {showHTTPConfigModal && ( -
-
-
-

HTTP Router Configuration

- -
-
-
-
- - setEntrypoints(e.target.value)} - className="w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500" - placeholder="websecure,metrics,api" - required - /> -

- Standard entrypoints: websecure (HTTPS), web (HTTP). Default: websecure -

-

- Note: Entrypoints must be defined in your Traefik static configuration file -

-
-
- - -
-
-
-
-
- )} - - {/* TLS Certificate Domains Modal */} - {showTLSConfigModal && ( -
-
-
-

TLS Certificate Domains

- -
-
-
-
- - setTLSDomains(e.target.value)} - className="w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500" - placeholder="example.com,*.example.com" - /> -

- Extra domains to include in the TLS certificate (Subject Alternative Names) -

-

- Main domain ({resource.host}) will be automatically included -

-
-
- - -
-
-
-
-
- )} - - {/* TCP SNI Routing Modal */} - {showTCPConfigModal && ( -
-
-
-

TCP SNI Routing Configuration

- -
-
-
-
- -

- Creates a separate TCP router with SNI matching rules -

-
- {tcpEnabled && ( - <> -
- - setTCPEntrypoints(e.target.value)} - className="w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500" - placeholder="tcp" - required - /> -

- Standard TCP entrypoint: tcp. Default: tcp -

-

- Note: Entrypoints must be defined in your Traefik static configuration file -

-
-
- - setTCPSNIRule(e.target.value)} - className="w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500" - placeholder={`HostSNI(\`${resource.host}\`)`} - /> -

- SNI rule using HostSNI or HostSNIRegexp matchers -

-

Examples:

-
    -
  • Match specific domain: {`HostSNI(\`${resource.host}\`)`}
  • -
  • Match with wildcard: {`HostSNIRegexp(\`^.+\\.example\\.com$\`)`}
  • -
  • Complex rule: {`HostSNI(\`${resource.host}\`) || (HostSNI(\`other.example.com\`) && !ALPN(\`h2\`))`}
  • -
-

- If empty, defaults to {`HostSNI(\`${resource.host}\`)`} -

-
- - )} -
- - -
-
-
-
-
- )} - {/* Add Custom Headers Modal here */} - {showHeadersConfigModal && ( -
-
-
-

Custom Headers Configuration

- -
-
-
-
- - - {/* Current headers list */} - {Object.keys(customHeaders).length > 0 ? ( -
-

Current Headers

-
    - {Object.entries(customHeaders).map(([key, value]) => ( -
  • -
    - {key}: {value} -
    - -
  • - ))} -
-
- ) : ( -

No custom headers configured.

- )} - - {/* Add new header */} -
-

Add New Header

-
-
- setHeaderKey(e.target.value)} - placeholder="Header name" - className="w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500" - /> -
-
- setHeaderValue(e.target.value)} - placeholder="Header value" - className="w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500" - /> -
-
- -
-
-

- Common examples: Host, X-Forwarded-Host -

-

- Host: To modify the hostname sent to the backend service -

-

- X-Forwarded-Host: To pass the original hostname to the backend -

-
-
- -
- - -
-
-
-
-
- )} -
- ); -}; - -// --- Middlewares List Component --- -const MiddlewaresList = ({ navigateTo }) => { - const [middlewares, setMiddlewares] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [searchTerm, setSearchTerm] = useState(''); - const [showDeleteModal, setShowDeleteModal] = useState(false); - const [middlewareToDelete, setMiddlewareToDelete] = useState(null); - - // Fetch all middlewares - const fetchMiddlewares = async () => { - try { - setLoading(true); - const data = await api.getMiddlewares(); - setMiddlewares(data); - setError(null); - } catch (err) { - setError('Failed to load middlewares'); - console.error('Middlewares fetch error:', err); - } finally { - setLoading(false); - } - }; - - useEffect(() => { - fetchMiddlewares(); - }, []); - - // Open confirmation modal before deleting - const confirmDelete = (middleware) => { - setMiddlewareToDelete(middleware); - setShowDeleteModal(true); - }; - - // Handle middleware deletion after confirmation - const handleDeleteMiddleware = async () => { - if (!middlewareToDelete) return; - - try { - await api.deleteMiddleware(middlewareToDelete.id); - setShowDeleteModal(false); - setMiddlewareToDelete(null); - await fetchMiddlewares(); - } catch (err) { - alert('Failed to delete middleware'); - console.error('Delete middleware error:', err); - } - }; - - // Cancel deletion - const cancelDelete = () => { - setShowDeleteModal(false); - setMiddlewareToDelete(null); - }; - - // Filter middlewares based on search term - const filteredMiddlewares = middlewares.filter( - (middleware) => - middleware.name.toLowerCase().includes(searchTerm.toLowerCase()) || - middleware.type.toLowerCase().includes(searchTerm.toLowerCase()) - ); - - return ( -
-

Middlewares

-
-
- setSearchTerm(e.target.value)} - /> -
-
- - -
-
- {loading && !middlewares.length ? ( -
Loading...
- ) : error ? ( -
{error}
- ) : ( -
- - - - - - - - - - {filteredMiddlewares.map((middleware) => ( - - - - - - ))} - {filteredMiddlewares.length === 0 && ( - - - - )} - -
- Name - - Type - - Actions -
- {middleware.name} - - - {middleware.type} - {middleware.type === 'chain' && " (Middleware Chain)"} - - -
- - -
-
- No middlewares found -
-
- )} - - {/* Delete Confirmation Modal */} - {showDeleteModal && ( -
-
-
-

Confirm Deletion

-
-
-

- Are you sure you want to delete the middleware "{middlewareToDelete?.name}"? -

-

- This action cannot be undone and may affect any resources currently using this middleware. -

-
- - -
-
-
-
- )} -
- ); -}; - -// --- Middleware Form Component --- -const MiddlewareForm = ({ id, isEditing, navigateTo }) => { - const [middleware, setMiddleware] = useState({ - name: '', - type: 'basicAuth', - config: {} - }); - const [configText, setConfigText] = useState('{\n "users": [\n "admin:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/"\n ]\n}'); - const [loading, setLoading] = useState(isEditing); - const [error, setError] = useState(null); - - // Add state for available middlewares and selected middlewares (for chain type) - const [availableMiddlewares, setAvailableMiddlewares] = useState([]); - const [selectedMiddlewares, setSelectedMiddlewares] = useState([]); - - // Available middleware types - const middlewareTypes = [ - { value: 'basicAuth', label: 'Basic Authentication' }, - { value: 'digestAuth', label: 'Digest Authentication' }, - { value: 'forwardAuth', label: 'Forward Authentication' }, - { value: 'ipWhiteList', label: 'IP Whitelist' }, - { value: 'ipAllowList', label: 'IP Allow List' }, - { value: 'rateLimit', label: 'Rate Limiting' }, - { value: 'headers', label: 'HTTP Headers' }, - { value: 'stripPrefix', label: 'Strip Prefix' }, - { value: 'stripPrefixRegex', label: 'Strip Prefix Regex' }, - { value: 'addPrefix', label: 'Add Prefix' }, - { value: 'redirectRegex', label: 'Redirect Regex' }, - { value: 'redirectScheme', label: 'Redirect Scheme' }, - { value: 'replacePath', label: 'Replace Path' }, - { value: 'replacePathRegex', label: 'Replace Path Regex' }, - { value: 'chain', label: 'Middleware Chain' }, - { value: 'plugin', label: 'Traefik Plugin' }, - { value: 'buffering', label: 'Buffering' }, - { value: 'circuitBreaker', label: 'Circuit Breaker' }, - { value: 'compress', label: 'Compression' }, - { value: 'contentType', label: 'Content Type' }, - { value: 'errors', label: 'Error Pages' }, - { value: 'grpcWeb', label: 'gRPC Web' }, - { value: 'inFlightReq', label: 'In-Flight Request Limiter' }, - { value: 'passTLSClientCert', label: 'Pass TLS Client Certificate' }, - { value: 'retry', label: 'Retry' } - ]; - - // Template configs for different middleware types - const configTemplates = { - basicAuth: '{\n "users": [\n "admin:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/"\n ]\n}', - digestAuth: '{\n "users": [\n "test:traefik:a2688e031edb4be6a3797f3882655c05"\n ]\n}', - forwardAuth: '{\n "address": "http://auth-service:9090/auth",\n "trustForwardHeader": true,\n "authResponseHeaders": [\n "X-Auth-User",\n "X-Auth-Roles"\n ]\n}', - ipWhiteList: '{\n "sourceRange": [\n "127.0.0.1/32",\n "192.168.1.0/24"\n ]\n}', - ipAllowList: '{\n "sourceRange": [\n "127.0.0.1/32",\n "192.168.1.0/24"\n ]\n}', - rateLimit: '{\n "average": 100,\n "burst": 50\n}', - headers: '{\n "browserXssFilter": true,\n "contentTypeNosniff": true,\n "customFrameOptionsValue": "SAMEORIGIN",\n "forceSTSHeader": true,\n "stsIncludeSubdomains": true,\n "stsSeconds": 63072000,\n "customResponseHeaders": {\n "X-Custom-Header": "value",\n "Server": ""\n }\n}', - stripPrefix: '{\n "prefixes": [\n "/api"\n ],\n "forceSlash": true\n}', - addPrefix: '{\n "prefix": "/api"\n}', - redirectRegex: '{\n "regex": "^http://(.*)$",\n "replacement": "https://${1}",\n "permanent": true\n}', - redirectScheme: '{\n "scheme": "https",\n "permanent": true,\n "port": "443"\n}', - chain: '{\n "middlewares": []\n}', - replacePath: '{\n "path": "/newpath"\n}', - replacePathRegex: '{\n "regex": "^/api/(.*)",\n "replacement": "/bar/$1"\n}', - stripPrefixRegex: '{\n "regex": [\n "^/api/v\\\\d+/"\n ]\n}', - plugin: '{\n "plugin-name": {\n "option1": "value1",\n "option2": "value2"\n }\n}', - buffering: '{\n "maxRequestBodyBytes": 5000000,\n "memRequestBodyBytes": 2000000,\n "maxResponseBodyBytes": 5000000,\n "memResponseBodyBytes": 2000000,\n "retryExpression": "IsNetworkError() && Attempts() < 2"\n}', - circuitBreaker: '{\n "expression": "NetworkErrorRatio() > 0.20 || ResponseCodeRatio(500, 600, 0, 600) > 0.25",\n "checkPeriod": "10s",\n "fallbackDuration": "30s",\n "recoveryDuration": "60s"\n}', - compress: '{\n "excludedContentTypes": [\n "text/event-stream"\n ],\n "minResponseBodyBytes": 1024\n}', - contentType: '{}', - errors: '{\n "status": ["500-599"],\n "service": "error-handler-service",\n "query": "/{status}.html"\n}', - grpcWeb: '{\n "allowOrigins": ["*"]\n}', - inFlightReq: '{\n "amount": 10,\n "sourceCriterion": {\n "ipStrategy": {\n "depth": 2,\n "excludedIPs": ["127.0.0.1/32"]\n }\n }\n}', - passTLSClientCert: '{\n "pem": true\n}', - retry: '{\n "attempts": 3,\n "initialInterval": "100ms"\n}' - }; - - // Custom JSON parser that preserves empty strings - const parseJSONPreservingEmptyStrings = (jsonString) => { - return JSON.parse(jsonString, (key, value) => { - // Explicitly preserve empty strings - if (value === "") return ""; - return value; - }); - }; - - // Custom JSON stringifier to format with proper indentation - // and ensure empty strings are preserved - const stringifyJSONWithEmptyStrings = (obj) => { - return JSON.stringify(obj, (key, value) => { - // Ensure empty strings are preserved - if (value === "") return ""; - return value; - }, 2); - }; - - // Function to fetch available middlewares - const fetchAvailableMiddlewares = async () => { - try { - const response = await api.getMiddlewares(); - // Filter out the current middleware if we're editing - const middlewares = isEditing - ? response.filter(mw => mw.id !== id) - : response; - - setAvailableMiddlewares(middlewares); - } catch (err) { - console.error("Error fetching middlewares:", err); - setError("Failed to load available middlewares"); - } - }; - - // Fetch middleware details if editing - useEffect(() => { - // First, fetch all available middlewares if we're dealing with chain type - if (middleware.type === 'chain' || !isEditing) { - fetchAvailableMiddlewares(); - } - - if (isEditing && id) { - const fetchMiddleware = async () => { - try { - setLoading(true); - const data = await api.getMiddleware(id); - setMiddleware({ - name: data.name, - type: data.type, - config: data.config - }); - - // Format config as pretty JSON string, preserving empty strings - const configJson = typeof data.config === 'string' - ? data.config - : stringifyJSONWithEmptyStrings(data.config); - - setConfigText(configJson); - - // If this is a chain middleware, fetch available middlewares and parse the selected ones - if (data.type === 'chain') { - fetchAvailableMiddlewares(); - - // Extract the middleware IDs from the config - if (data.config && data.config.middlewares) { - setSelectedMiddlewares(data.config.middlewares); - } - } - - setError(null); - } catch (err) { - setError('Failed to load middleware details'); - console.error('Middleware fetch error:', err); - } finally { - setLoading(false); - } - }; - - fetchMiddleware(); - } - }, [id, isEditing, middleware.type]); - - // Update config template when type changes - const handleTypeChange = (e) => { - const newType = e.target.value; - setMiddleware({ ...middleware, type: newType }); - - // For chain type, initialize with empty middlewares array and fetch available middlewares - if (newType === 'chain') { - setConfigText('{\n "middlewares": []\n}'); - setSelectedMiddlewares([]); - fetchAvailableMiddlewares(); - } else { - setConfigText(configTemplates[newType] || '{}'); - } - }; - - // Handle selection of middlewares for chain type - const handleMiddlewareSelection = (e) => { - const options = e.target.options; - const selected = Array.from(options) - .filter(option => option.selected) - .map(option => option.value); - - setSelectedMiddlewares(selected); - - // Update the config text to reflect the selected middlewares - const configObj = { - middlewares: selected - }; - setConfigText(stringifyJSONWithEmptyStrings(configObj)); - }; - - // Handle form submission - const handleSubmit = async (e) => { - e.preventDefault(); - - try { - // Parse config JSON, preserving empty strings - let configObj; - - if (middleware.type === 'chain') { - // For chain type, create configuration from selected middlewares - configObj = { - middlewares: selectedMiddlewares - }; - } else { - try { - configObj = parseJSONPreservingEmptyStrings(configText); - } catch (err) { - alert('Invalid JSON configuration. Please check the format.'); - return; - } - } - - // Process specific middleware types to ensure correct data types - // and string formats are preserved - switch (middleware.type) { - case 'headers': - // Ensure empty strings are preserved in headers - processHeadersConfig(configObj); - break; - case 'redirectRegex': - case 'redirectScheme': - case 'replacePath': - case 'replacePathRegex': - case 'stripPrefix': - case 'stripPrefixRegex': - // Ensure regex patterns are properly formatted - processPathConfig(configObj, middleware.type); - break; - case 'rateLimit': - case 'inFlightReq': - // Ensure numeric values are actually numbers, not strings - processNumericConfig(configObj); - break; - case 'plugin': - // Special handling for plugin configs - processPluginConfig(configObj); - break; - } - - const middlewareData = { - name: middleware.name, - type: middleware.type, - config: configObj - }; - - setLoading(true); - - if (isEditing) { - await api.updateMiddleware(id, middlewareData); - alert('Middleware updated successfully'); - } else { - await api.createMiddleware(middlewareData); - alert('Middleware created successfully'); - } - - navigateTo('middlewares'); - } catch (err) { - setError(`Failed to ${isEditing ? 'update' : 'create'} middleware`); - console.error('Middleware form error:', err); - alert(`Error: ${err.message || 'Unknown error occurred'}`); - } finally { - setLoading(false); - } - }; - - // Process headers configuration to ensure empty strings are preserved - const processHeadersConfig = (config) => { - // Process customResponseHeaders - if (config.customResponseHeaders) { - Object.keys(config.customResponseHeaders).forEach(key => { - if (config.customResponseHeaders[key] === "") { - // Explicitly set empty strings to ensure they're preserved - config.customResponseHeaders[key] = ""; - } - }); - } - - // Process customRequestHeaders - if (config.customRequestHeaders) { - Object.keys(config.customRequestHeaders).forEach(key => { - if (config.customRequestHeaders[key] === "") { - // Explicitly set empty strings to ensure they're preserved - config.customRequestHeaders[key] = ""; - } - }); - } - }; - - // Process path-related configurations - const processPathConfig = (config, type) => { - // Regex patterns need special handling to ensure they're properly formatted - if (config.regex) { - // If regex is an array (like in stripPrefixRegex) - if (Array.isArray(config.regex)) { - config.regex = config.regex.map(pattern => String(pattern)); - } else { - // Ensure regex is a string - config.regex = String(config.regex); - } - } - - // Ensure replacement is a string - if (config.replacement) { - config.replacement = String(config.replacement); - } - - // Ensure path is a string - if (config.path) { - config.path = String(config.path); - } - }; - - // Process numeric configurations - const processNumericConfig = (config) => { - // Convert numeric string values to actual numbers - if (config.average && typeof config.average === 'string') { - config.average = Number(config.average); - } - - if (config.burst && typeof config.burst === 'string') { - config.burst = Number(config.burst); - } - - if (config.amount && typeof config.amount === 'string') { - config.amount = Number(config.amount); - } - - // Handle nested ipStrategy.depth - if (config.sourceCriterion?.ipStrategy?.depth && - typeof config.sourceCriterion.ipStrategy.depth === 'string') { - config.sourceCriterion.ipStrategy.depth = Number(config.sourceCriterion.ipStrategy.depth); - } - }; - - // Process plugin configurations - const processPluginConfig = (config) => { - // Iterate through plugin objects - Object.keys(config).forEach(pluginName => { - const pluginConfig = config[pluginName]; - - if (typeof pluginConfig === 'object') { - // Process API keys to ensure they're preserved exactly - const keyFields = ['key', 'apiKey', 'token', 'secret', 'password', 'crowdsecLapiKey']; - - keyFields.forEach(field => { - if (field in pluginConfig && pluginConfig[field] === "") { - // Explicitly preserve empty strings - pluginConfig[field] = ""; - } - }); - } - }); - }; - - if (loading && isEditing) { - return
Loading...
; - } - - return ( -
-
- -

- {isEditing ? 'Edit Middleware' : 'Create Middleware'} -

-
- - {error && ( -
{error}
- )} - -
-
-
- - setMiddleware({ ...middleware, name: e.target.value })} - className="w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500" - placeholder="e.g., production-authentication" - required - /> -
- -
- - - {isEditing && ( -

- Middleware type cannot be changed after creation -

- )} -
- - {middleware.type === 'chain' ? ( -
- - {availableMiddlewares.length > 0 ? ( - <> - -

- Hold Ctrl (or Cmd) to select multiple middlewares. Middlewares will be applied in the order selected. -

- - {/* Display selected middlewares */} - {selectedMiddlewares.length > 0 ? ( -
-

Chain Execution Order:

-
    - {selectedMiddlewares.map((mwId) => { - const mw = availableMiddlewares.find(m => m.id === mwId); - return ( -
  1. - {mw ? `${mw.name} (${mw.type})` : mwId} -
  2. - ); - })} -
-
- ) : ( -
-

No middlewares selected. Please select at least one middleware to create a chain.

-
- )} - - ) : ( -
-

You need to create other middlewares first before creating a chain.

- -
- )} -
- ) : ( -
- -