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 ( -
{initializationPhase}
- {retryCount > 0 && ( -Attempt {retryCount} of {maxRetries}
-The initial startup may take a moment while background services initialize.
-{error}
- -{activeResources}
- {disabledResources > 0 && ( -- {disabledResources} disabled resources -
- )} -{middlewares.length}
-- {protectedResources} / {activeResources} -
-| - 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 - | -|||
- You have {unprotectedResources} active resources that are not - protected with any middleware. -
-- You have {disabledResources} disabled resources that were removed - from Pangolin.{' '} - navigateTo('resources')} - > - View all resources - {' '} - to delete them. -
-| - 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 - | -|||
- This resource has been removed from Pangolin and is now disabled. Any changes to middleware will not take effect. -
-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}
-HTTP Entrypoints
-{entrypoints || 'websecure'}
-TLS Certificate Domains
-{tlsDomains || 'None'}
-TCP SNI Routing
-{tcpEnabled ? 'Enabled' : 'Disabled'}
-TCP Entrypoints
-{tcpEntrypoints || 'tcp'}
-TCP SNI Rule
-{tcpSNIRule}
-Custom Headers
-- 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. -
-This resource does not have any middlewares applied to it.
-Add a middleware to enhance security or modify behavior.
-| - Middleware - | -- Priority - | -- Actions - | -
|---|---|---|
| - {formatMiddlewareDisplay(middlewareDetails, middlewares)} - | -- {middleware.priority} - | -- - | -
All middlewares have been assigned to this resource.
- -| - Name - | -- Type - | -- Actions - | -
|---|---|---|
| - {middleware.name} - | -- - {middleware.type} - {middleware.type === 'chain' && " (Middleware Chain)"} - - | -
-
-
-
-
- |
-
| - No middlewares found - | -||
- 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. -
-{details}
+{message}
+ )} +| + Host + | ++ Status + | ++ Middlewares + | ++ Actions + | +
|---|---|---|---|
| + No resources found + | +|||
+ You have {unprotectedResources} active resources that are not + protected with any middleware. +
++ You have {disabledResources} disabled resources that were removed + from Pangolin.{' '} + {' '} + to delete them. +
+{value}
+ {subtitle && ( ++ {subtitle} +
+ )} +| + Name + | ++ Type + | ++ Actions + | +
|---|---|---|
| + {middleware.name} + | ++ + {middleware.type} + {middleware.type === 'chain' && " (Middleware Chain)"} + + | +
+
+
+
+
+ |
+
| + No middlewares found + | +||
+ 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. +
+All middlewares have been assigned to this resource.
+ ++ This resource has been removed from Pangolin and is now disabled. Any changes to middleware will not take effect. +
+Host
++ {selectedResource.host} + + Visit + +
+Service ID
+{selectedResource.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
+{selectedResource.id}
+HTTP Entrypoints
+{entrypoints || 'websecure'}
+TLS Certificate Domains
+{tlsDomains || 'None'}
+TCP SNI Routing
+{tcpEnabled ? 'Enabled' : 'Disabled'}
+TCP Entrypoints
+{tcpEntrypoints || 'tcp'}
+TCP SNI Rule
+{tcpSNIRule}
+Custom Headers
++ 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. +
+This resource does not have any middlewares applied to it.
+Add a middleware to enhance security or modify behavior.
+| + Middleware + | ++ Priority + | ++ Actions + | +
|---|---|---|
| + {middlewareDetails && middlewares.length > 0 + ? formatMiddlewareDisplay(middlewareDetails) + : middleware.name} + | ++ {middleware.priority} + | ++ + | +
| + 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 + | +|||