You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
spiderman/internal/api/handler.go

573 lines
17 KiB

// Package api provides HTTP API handlers
package api
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"spiderman/internal/fabric"
"spiderman/internal/logger"
"spiderman/pkg/models"
"github.com/google/uuid"
"github.com/gorilla/mux"
"github.com/sirupsen/logrus"
)
// Handler handles HTTP requests and communicates with Fabric
type Handler struct {
fabricClient *fabric.Client
}
// NewHandler creates a new API handler
func NewHandler(fabricClient *fabric.Client) *Handler {
logger.Logger.Info("API handler initialized")
return &Handler{
fabricClient: fabricClient,
}
}
// writeJSONResponse writes a JSON response to the HTTP response writer
func writeJSONResponse(w http.ResponseWriter, statusCode int, response models.Response) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
json.NewEncoder(w).Encode(response)
}
// writeErrorResponse writes an error response to the HTTP response writer
func writeErrorResponse(w http.ResponseWriter, statusCode int, message string) {
writeJSONResponse(w, statusCode, models.Response{
Success: false,
Message: message,
})
}
// HealthCheck godoc
//
// @Summary Health check
// @Description Check the health status of the API
// @Tags health
// @Accept json
// @Produce json
// @Success 200 {object} models.HealthResponse
// @Router /health [get]
func (h *Handler) HealthCheck(w http.ResponseWriter, r *http.Request) {
requestLogger := logger.GetRequestLogger(r.Method, r.URL.Path, r.RemoteAddr)
requestLogger.Debug("Processing health check request")
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(models.HealthResponse{Status: "healthy"})
requestLogger.Info("Health check completed successfully")
}
// InitLedger godoc
//
// @Summary Initialize ledger
// @Description Initialize the blockchain ledger with a set of assets
// @Tags ledger
// @Accept json
// @Produce json
// @Success 200 {object} models.Response
// @Failure 500 {object} models.ErrorResponse
// @Router /ledger/init [post]
func (h *Handler) InitLedger(w http.ResponseWriter, r *http.Request) {
requestLogger := logger.GetRequestLogger(r.Method, r.URL.Path, r.RemoteAddr)
requestLogger.Info("Starting ledger initialization")
start := time.Now()
err := h.fabricClient.InitLedger()
duration := time.Since(start)
if err != nil {
requestLogger.WithFields(logrus.Fields{
"error": err.Error(),
"duration_ms": duration.Milliseconds(),
}).Error("Failed to initialize ledger")
writeErrorResponse(w, http.StatusInternalServerError,
fmt.Sprintf("Failed to initialize ledger: %s", fabric.HandleFabricError(err)))
return
}
requestLogger.WithFields(logrus.Fields{
"duration_ms": duration.Milliseconds(),
}).Info("Ledger initialized successfully")
writeJSONResponse(w, http.StatusOK, models.Response{
Success: true,
Message: "Ledger initialized successfully",
})
}
// GetAllAssets godoc
//
// @Summary Get all assets
// @Description Retrieve all assets from the blockchain ledger
// @Tags assets
// @Accept json
// @Produce json
// @Success 200 {object} models.Response{data=[]models.Asset}
// @Failure 500 {object} models.ErrorResponse
// @Router /assets [get]
func (h *Handler) GetAllAssets(w http.ResponseWriter, r *http.Request) {
requestLogger := logger.GetRequestLogger(r.Method, r.URL.Path, r.RemoteAddr)
requestLogger.Info("Retrieving all assets")
start := time.Now()
assets, err := h.fabricClient.GetAllAssets()
duration := time.Since(start)
if err != nil {
requestLogger.WithFields(logrus.Fields{
"error": err.Error(),
"duration_ms": duration.Milliseconds(),
}).Error("Failed to retrieve assets")
writeErrorResponse(w, http.StatusInternalServerError,
fmt.Sprintf("Failed to get assets: %s", fabric.HandleFabricError(err)))
return
}
requestLogger.WithFields(logrus.Fields{
"asset_count": len(assets),
"duration_ms": duration.Milliseconds(),
}).Info("Assets retrieved successfully")
writeJSONResponse(w, http.StatusOK, models.Response{
Success: true,
Data: assets,
})
}
// CreateAsset godoc
//
// @Summary Create a new asset
// @Description Create a new asset on the blockchain ledger with auto-generated UUID if ID not provided
// @Tags assets
// @Accept json
// @Produce json
// @Param asset body models.CreateAssetRequest true "Asset data (ID is optional and will be auto-generated)"
// @Success 201 {object} models.Response{data=object{asset=models.Asset,transactionId=string}}
// @Failure 400 {object} models.ErrorResponse
// @Failure 500 {object} models.ErrorResponse
// @Router /assets [post]
func (h *Handler) CreateAsset(w http.ResponseWriter, r *http.Request) {
requestLogger := logger.GetRequestLogger(r.Method, r.URL.Path, r.RemoteAddr)
requestLogger.Info("Creating new asset")
var req models.CreateAssetRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
requestLogger.WithError(err).Warn("Invalid request body for asset creation")
writeErrorResponse(w, http.StatusBadRequest, "Invalid request body")
return
}
// Auto-generate UUID if ID is not provided
if req.ID == "" {
req.ID = uuid.New().String()
requestLogger.WithField("generated_id", req.ID).Info("Auto-generated UUID for asset")
}
requestLogger.WithFields(logrus.Fields{
"asset_id": req.ID,
"owner": req.Owner,
"color": req.Color,
"size": req.Size,
"value": req.AppraisedValue,
}).Debug("Asset creation request details")
// Convert request to asset model
asset := models.Asset{
ID: req.ID,
Color: req.Color,
Size: req.Size,
Owner: req.Owner,
AppraisedValue: req.AppraisedValue,
}
start := time.Now()
transactionResult, err := h.fabricClient.CreateAsset(asset)
duration := time.Since(start)
if err != nil {
requestLogger.WithFields(logrus.Fields{
"error": err.Error(),
"asset_id": req.ID,
"duration_ms": duration.Milliseconds(),
}).Error("Failed to create asset")
writeErrorResponse(w, http.StatusInternalServerError,
fmt.Sprintf("Failed to create asset: %s", fabric.HandleFabricError(err)))
return
}
requestLogger.WithFields(logrus.Fields{
"asset_id": req.ID,
"transaction_result": transactionResult,
"duration_ms": duration.Milliseconds(),
}).Info("Asset created successfully")
// Create response data that includes both asset and transaction ID
responseData := map[string]interface{}{
"asset": asset,
"transactionId": transactionResult,
}
writeJSONResponse(w, http.StatusCreated, models.Response{
Success: true,
Message: "Asset created successfully",
Data: responseData,
})
}
// GetAssetByID godoc
//
// @Summary Get asset by ID
// @Description Retrieve a specific asset by its ID from the blockchain ledger
// @Tags assets
// @Accept json
// @Produce json
// @Param id path string true "Asset ID"
// @Success 200 {object} models.Response{data=models.Asset}
// @Failure 404 {object} models.ErrorResponse
// @Failure 500 {object} models.ErrorResponse
// @Router /assets/{id} [get]
func (h *Handler) GetAssetByID(w http.ResponseWriter, r *http.Request) {
requestLogger := logger.GetRequestLogger(r.Method, r.URL.Path, r.RemoteAddr)
vars := mux.Vars(r)
assetID := vars["id"]
requestLogger.WithField("asset_id", assetID).Info("Retrieving asset by ID")
start := time.Now()
asset, err := h.fabricClient.ReadAssetByID(assetID)
duration := time.Since(start)
if err != nil {
requestLogger.WithFields(logrus.Fields{
"error": err.Error(),
"asset_id": assetID,
"duration_ms": duration.Milliseconds(),
}).Error("Failed to retrieve asset")
statusCode := http.StatusInternalServerError
if err.Error() == "asset not found" {
statusCode = http.StatusNotFound
}
writeErrorResponse(w, statusCode,
fmt.Sprintf("Failed to get asset: %s", fabric.HandleFabricError(err)))
return
}
requestLogger.WithFields(logrus.Fields{
"asset_id": assetID,
"duration_ms": duration.Milliseconds(),
}).Info("Asset retrieved successfully")
writeJSONResponse(w, http.StatusOK, models.Response{
Success: true,
Data: asset,
})
}
// TransferAsset godoc
//
// @Summary Transfer asset ownership
// @Description Transfer ownership of an asset to a new owner
// @Tags assets
// @Accept json
// @Produce json
// @Param id path string true "Asset ID"
// @Param request body models.TransferAssetRequest true "Transfer request"
// @Success 200 {object} models.Response
// @Failure 400 {object} models.ErrorResponse
// @Failure 404 {object} models.ErrorResponse
// @Failure 500 {object} models.ErrorResponse
// @Router /assets/{id}/transfer [put]
func (h *Handler) TransferAsset(w http.ResponseWriter, r *http.Request) {
requestLogger := logger.GetRequestLogger(r.Method, r.URL.Path, r.RemoteAddr)
vars := mux.Vars(r)
assetID := vars["id"]
requestLogger.WithField("asset_id", assetID).Info("Transferring asset")
var req models.TransferAssetRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
requestLogger.WithError(err).Warn("Invalid request body for asset transfer")
writeErrorResponse(w, http.StatusBadRequest, "Invalid request body")
return
}
requestLogger.WithFields(logrus.Fields{
"asset_id": assetID,
"new_owner": req.NewOwner,
}).Debug("Asset transfer request details")
start := time.Now()
err := h.fabricClient.TransferAsset(assetID, req.NewOwner)
duration := time.Since(start)
if err != nil {
requestLogger.WithFields(logrus.Fields{
"error": err.Error(),
"asset_id": assetID,
"new_owner": req.NewOwner,
"duration_ms": duration.Milliseconds(),
}).Error("Failed to transfer asset")
statusCode := http.StatusInternalServerError
if err.Error() == "asset not found" {
statusCode = http.StatusNotFound
}
writeErrorResponse(w, statusCode,
fmt.Sprintf("Failed to transfer asset: %s", fabric.HandleFabricError(err)))
return
}
requestLogger.WithFields(logrus.Fields{
"asset_id": assetID,
"new_owner": req.NewOwner,
"duration_ms": duration.Milliseconds(),
}).Info("Asset transferred successfully")
writeJSONResponse(w, http.StatusOK, models.Response{
Success: true,
Message: "Asset transferred successfully",
})
}
// UpdateAsset godoc
//
// @Summary Update an existing asset
// @Description Update an existing asset's information on the blockchain
// @Tags assets
// @Accept json
// @Produce json
// @Param id path string true "Asset ID"
// @Param asset body models.CreateAssetRequest true "Updated asset data"
// @Success 200 {object} models.Response{data=models.Asset}
// @Failure 400 {object} models.ErrorResponse
// @Failure 500 {object} models.ErrorResponse
// @Router /assets/{id} [put]
func (h *Handler) UpdateAsset(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
assetID := vars["id"]
requestLogger := logger.GetRequestLogger(r.Method, r.URL.Path, r.RemoteAddr).
WithField("asset_id", assetID)
requestLogger.Info("Updating asset")
var req models.CreateAssetRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
requestLogger.WithError(err).Warn("Invalid request body for asset update")
writeErrorResponse(w, http.StatusBadRequest, "Invalid request body")
return
}
// Set the ID from the URL
req.ID = assetID
requestLogger.WithFields(logrus.Fields{
"owner": req.Owner,
"color": req.Color,
"size": req.Size,
"value": req.AppraisedValue,
}).Debug("Asset update request details")
// Convert request to asset model
asset := models.Asset{
ID: req.ID,
Color: req.Color,
Size: req.Size,
Owner: req.Owner,
AppraisedValue: req.AppraisedValue,
}
start := time.Now()
err := h.fabricClient.UpdateAsset(asset)
duration := time.Since(start)
if err != nil {
requestLogger.WithFields(logrus.Fields{
"error": err.Error(),
"duration_ms": duration.Milliseconds(),
}).Error("Failed to update asset")
writeErrorResponse(w, http.StatusInternalServerError,
fmt.Sprintf("Failed to update asset: %s", fabric.HandleFabricError(err)))
return
}
requestLogger.WithFields(logrus.Fields{
"duration_ms": duration.Milliseconds(),
}).Info("Asset updated successfully")
writeJSONResponse(w, http.StatusOK, models.Response{
Success: true,
Message: "Asset updated successfully",
Data: asset,
})
}
// GetBlockHeight godoc
//
// @Summary Get block height
// @Description Get the current block height of the blockchain
// @Tags blockchain
// @Accept json
// @Produce json
// @Success 200 {object} models.Response{data=models.BlockHeightResponse}
// @Failure 500 {object} models.ErrorResponse
// @Router /blockchain/height [get]
func (h *Handler) GetBlockHeight(w http.ResponseWriter, r *http.Request) {
requestLogger := logger.GetRequestLogger(r.Method, r.URL.Path, r.RemoteAddr)
requestLogger.Info("Getting block height")
start := time.Now()
height, err := h.fabricClient.GetBlockHeight()
duration := time.Since(start)
if err != nil {
requestLogger.WithFields(logrus.Fields{
"error": err.Error(),
"duration_ms": duration.Milliseconds(),
}).Error("Failed to get block height")
writeErrorResponse(w, http.StatusInternalServerError,
fmt.Sprintf("Failed to get block height: %s", fabric.HandleFabricError(err)))
return
}
requestLogger.WithFields(logrus.Fields{
"height": height,
"duration_ms": duration.Milliseconds(),
}).Info("Block height retrieved successfully")
response := models.BlockHeightResponse{
Height: height,
}
writeJSONResponse(w, http.StatusOK, models.Response{
Success: true,
Message: "Block height retrieved successfully",
Data: response,
})
}
// GetChainInfo godoc
//
// @Summary Get chain information
// @Description Get detailed information about the blockchain including block height
// @Tags blockchain
// @Accept json
// @Produce json
// @Success 200 {object} models.Response{data=models.ChainInfoResponse}
// @Failure 500 {object} models.ErrorResponse
// @Router /blockchain/info [get]
func (h *Handler) GetChainInfo(w http.ResponseWriter, r *http.Request) {
requestLogger := logger.GetRequestLogger(r.Method, r.URL.Path, r.RemoteAddr)
requestLogger.Info("Getting chain information")
start := time.Now()
height, err := h.fabricClient.GetBlockHeight()
duration := time.Since(start)
if err != nil {
requestLogger.WithFields(logrus.Fields{
"error": err.Error(),
"duration_ms": duration.Milliseconds(),
}).Error("Failed to get chain information")
writeErrorResponse(w, http.StatusInternalServerError,
fmt.Sprintf("Failed to get chain information: %s", fabric.HandleFabricError(err)))
return
}
requestLogger.WithFields(logrus.Fields{
"height": height,
"duration_ms": duration.Milliseconds(),
}).Info("Chain information retrieved successfully")
response := models.ChainInfoResponse{
Height: height,
ChainName: h.fabricClient.GetChannelName(),
}
writeJSONResponse(w, http.StatusOK, models.Response{
Success: true,
Message: "Chain information retrieved successfully",
Data: response,
})
}
// GetTransactionByID godoc
//
// @Summary Get transaction details by ID
// @Description Retrieve detailed information about a specific transaction by its ID
// @Tags transactions
// @Accept json
// @Produce json
// @Param txid path string true "Transaction ID"
// @Success 200 {object} models.Response{data=models.TransactionDetail}
// @Failure 400 {object} models.ErrorResponse
// @Failure 404 {object} models.ErrorResponse
// @Failure 500 {object} models.ErrorResponse
// @Router /transactions/{txid} [get]
func (h *Handler) GetTransactionByID(w http.ResponseWriter, r *http.Request) {
requestLogger := logger.GetRequestLogger(r.Method, r.URL.Path, r.RemoteAddr)
vars := mux.Vars(r)
txID := vars["txid"]
if txID == "" {
requestLogger.Warn("Missing transaction ID in request")
writeErrorResponse(w, http.StatusBadRequest, "Transaction ID is required")
return
}
requestLogger.WithField("transaction_id", txID).Info("Getting transaction details")
start := time.Now()
txDetail, err := h.fabricClient.GetTransactionByID(txID)
duration := time.Since(start)
if err != nil {
requestLogger.WithFields(logrus.Fields{
"error": err.Error(),
"transaction_id": txID,
"duration_ms": duration.Milliseconds(),
}).Error("Failed to get transaction details")
statusCode := http.StatusInternalServerError
message := fmt.Sprintf("Failed to get transaction details: %s", fabric.HandleFabricError(err))
// Check if it's a "not found" error
if strings.Contains(err.Error(), "not found") ||
strings.Contains(err.Error(), "does not exist") ||
strings.Contains(err.Error(), "no such transaction ID") {
statusCode = http.StatusNotFound
message = "Transaction not found"
}
writeErrorResponse(w, statusCode, message)
return
}
requestLogger.WithFields(logrus.Fields{
"transaction_id": txID,
"block_number": txDetail.BlockNumber,
"duration_ms": duration.Milliseconds(),
}).Info("Transaction details retrieved successfully")
writeJSONResponse(w, http.StatusOK, models.Response{
Success: true,
Message: "Transaction details retrieved successfully",
Data: txDetail,
})
}