AX: standardize mining handler naming
Some checks are pending
Security Scan / security (push) Waiting to run
Test / test (push) Waiting to run

This commit is contained in:
Virgil 2026-04-04 07:03:01 +00:00
parent aea1fc1d03
commit 757e0a9ce4
6 changed files with 279 additions and 279 deletions

View file

@ -38,26 +38,26 @@ func DefaultAuthConfig() AuthConfig {
// authConfig := AuthConfigFromEnv() // reads MINING_API_AUTH, MINING_API_USER, MINING_API_PASS, MINING_API_REALM
func AuthConfigFromEnv() AuthConfig {
config := DefaultAuthConfig()
authConfig := DefaultAuthConfig()
if os.Getenv("MINING_API_AUTH") == "true" {
config.Enabled = true
config.Username = os.Getenv("MINING_API_USER")
config.Password = os.Getenv("MINING_API_PASS")
authConfig.Enabled = true
authConfig.Username = os.Getenv("MINING_API_USER")
authConfig.Password = os.Getenv("MINING_API_PASS")
if config.Username == "" || config.Password == "" {
if authConfig.Username == "" || authConfig.Password == "" {
logging.Warn("API auth enabled but credentials not set", logging.Fields{
"hint": "Set MINING_API_USER and MINING_API_PASS environment variables",
})
config.Enabled = false
authConfig.Enabled = false
}
}
if realm := os.Getenv("MINING_API_REALM"); realm != "" {
config.Realm = realm
authConfig.Realm = realm
}
return config
return authConfig
}
// digestAuth := NewDigestAuth(authConfig); router.Use(digestAuth.Middleware()); defer digestAuth.Stop()
@ -74,7 +74,7 @@ func NewDigestAuth(config AuthConfig) *DigestAuth {
config: config,
stopChan: make(chan struct{}),
}
// Start nonce cleanup goroutine
// go digestAuth.cleanupNonces() // clears expired nonces every 5 minutes
go digestAuth.cleanupNonces()
return digestAuth
}
@ -88,57 +88,57 @@ func (digestAuth *DigestAuth) Stop() {
// router.Use(digestAuth.Middleware()) // enforces Digest or Basic auth on all routes
func (digestAuth *DigestAuth) Middleware() gin.HandlerFunc {
return func(c *gin.Context) {
return func(requestContext *gin.Context) {
if !digestAuth.config.Enabled {
c.Next()
requestContext.Next()
return
}
authHeader := c.GetHeader("Authorization")
authHeader := requestContext.GetHeader("Authorization")
if authHeader == "" {
digestAuth.sendChallenge(c)
digestAuth.sendChallenge(requestContext)
return
}
// Try digest auth first
if strings.HasPrefix(authHeader, "Digest ") {
if digestAuth.validateDigest(c, authHeader) {
c.Next()
if digestAuth.validateDigest(requestContext, authHeader) {
requestContext.Next()
return
}
digestAuth.sendChallenge(c)
digestAuth.sendChallenge(requestContext)
return
}
// Fall back to basic auth
if strings.HasPrefix(authHeader, "Basic ") {
if digestAuth.validateBasic(c, authHeader) {
c.Next()
if digestAuth.validateBasic(requestContext, authHeader) {
requestContext.Next()
return
}
}
digestAuth.sendChallenge(c)
digestAuth.sendChallenge(requestContext)
}
}
// digestAuth.sendChallenge(c) // writes WWW-Authenticate header and 401 JSON response
func (digestAuth *DigestAuth) sendChallenge(c *gin.Context) {
func (digestAuth *DigestAuth) sendChallenge(requestContext *gin.Context) {
nonce := digestAuth.generateNonce()
digestAuth.nonces.Store(nonce, time.Now())
challenge := `Digest realm="` + digestAuth.config.Realm + `", qop="auth", nonce="` + nonce + `", opaque="` + digestAuth.generateOpaque() + `"`
c.Header("WWW-Authenticate", challenge)
c.AbortWithStatusJSON(http.StatusUnauthorized, APIError{
requestContext.Header("WWW-Authenticate", challenge)
requestContext.AbortWithStatusJSON(http.StatusUnauthorized, APIError{
Code: "AUTH_REQUIRED",
Message: "Authentication required",
Suggestion: "Provide valid credentials using Digest or Basic authentication",
})
}
// valid := digestAuth.validateDigest(c, c.GetHeader("Authorization"))
func (digestAuth *DigestAuth) validateDigest(c *gin.Context, authHeader string) bool {
// valid := digestAuth.validateDigest(requestContext, requestContext.GetHeader("Authorization"))
func (digestAuth *DigestAuth) validateDigest(requestContext *gin.Context, authHeader string) bool {
params := parseDigestParams(authHeader[7:]) // Skip "Digest "
nonce := params["nonce"]
@ -163,7 +163,7 @@ func (digestAuth *DigestAuth) validateDigest(c *gin.Context, authHeader string)
// Calculate expected response
hashA1 := md5Hash(digestAuth.config.Username + ":" + digestAuth.config.Realm + ":" + digestAuth.config.Password)
hashA2 := md5Hash(c.Request.Method + ":" + params["uri"])
hashA2 := md5Hash(requestContext.Request.Method + ":" + params["uri"])
var expectedResponse string
if params["qop"] == "auth" {
@ -176,10 +176,10 @@ func (digestAuth *DigestAuth) validateDigest(c *gin.Context, authHeader string)
return subtle.ConstantTimeCompare([]byte(expectedResponse), []byte(params["response"])) == 1
}
// valid := digestAuth.validateBasic(c, c.GetHeader("Authorization"))
func (digestAuth *DigestAuth) validateBasic(c *gin.Context, authHeader string) bool {
// valid := digestAuth.validateBasic(requestContext, requestContext.GetHeader("Authorization"))
func (digestAuth *DigestAuth) validateBasic(requestContext *gin.Context, authHeader string) bool {
// Gin has built-in basic auth, but we do manual validation for consistency
username, password, ok := c.Request.BasicAuth()
username, password, ok := requestContext.Request.BasicAuth()
if !ok {
return false
}

View file

@ -4,8 +4,8 @@ import (
"net/http"
)
// respondWithError(c, http.StatusNotFound, ErrCodeMinerNotFound, "xmrig not found", err.Error())
// respondWithError(c, http.StatusConflict, ErrCodeMinerExists, "xmrig already running", "")
// respondWithError(requestContext, http.StatusNotFound, ErrCodeMinerNotFound, "xmrig not found", err.Error())
// respondWithError(requestContext, http.StatusConflict, ErrCodeMinerExists, "xmrig already running", "")
const (
ErrCodeMinerNotFound = "MINER_NOT_FOUND"
ErrCodeMinerExists = "MINER_EXISTS"
@ -74,7 +74,7 @@ func (e *MiningError) IsRetryable() bool {
return e.Retryable
}
// c.JSON(e.StatusCode(), e) // 404 for not-found, 500 for internal errors
// requestContext.JSON(e.StatusCode(), e) // 404 for not-found, 500 for internal errors
func (e *MiningError) StatusCode() int {
if e.HTTPStatus == 0 {
return http.StatusInternalServerError

View file

@ -56,12 +56,12 @@ type MinerEventData struct {
// hub.ServeWs(conn) // upgrades conn, creates wsClient, registers with hub
type wsClient struct {
conn *websocket.Conn
send chan []byte
hub *EventHub
miners map[string]bool // subscribed miners, "*" for all
minersMutex sync.RWMutex // protects miners map from concurrent access
closeOnce sync.Once
conn *websocket.Conn
send chan []byte
hub *EventHub
miners map[string]bool // subscribed miners, "*" for all
minersMutex sync.RWMutex // protects miners map from concurrent access
closeOnce sync.Once
}
// client.safeClose() // safe to call from multiple goroutines; channel closed exactly once
@ -377,7 +377,7 @@ func (client *wsClient) readPump() {
}
}
// if !hub.ServeWs(conn) { c.JSON(http.StatusServiceUnavailable, gin.H{"error": "connection limit reached"}) }
// if !hub.ServeWs(conn) { requestContext.JSON(http.StatusServiceUnavailable, gin.H{"error": "connection limit reached"}) }
func (hub *EventHub) ServeWs(conn *websocket.Conn) bool {
// Check connection limit
hub.mutex.RLock()

View file

@ -10,7 +10,7 @@ import (
)
// nodeService, err := NewNodeService()
// nodeService.SetupRoutes(router.Group("/api/v1/mining"))
// router.Group("/api/v1/mining").Group("/node").GET("/info", nodeService.handleNodeInfo)
type NodeService struct {
nodeManager *node.NodeManager
peerRegistry *node.PeerRegistry
@ -31,8 +31,8 @@ func NewNodeService() (*NodeService, error) {
return nil, err
}
config := node.DefaultTransportConfig()
transport := node.NewTransport(nodeManager, peerRegistry, config)
transportConfig := node.DefaultTransportConfig()
transport := node.NewTransport(nodeManager, peerRegistry, transportConfig)
nodeService := &NodeService{
nodeManager: nodeManager,
@ -46,16 +46,16 @@ func NewNodeService() (*NodeService, error) {
return nodeService, nil
}
// service.SetupRoutes(router.Group("/api/v1/mining")) // registers /node, /peers, /remote route groups
// router.Group("/api/v1/mining") exposes /node, /peers, and /remote route groups.
func (nodeService *NodeService) SetupRoutes(router *gin.RouterGroup) {
// Node identity endpoints
// router.Group("/node").GET("/info", nodeService.handleNodeInfo) exposes node identity and peer counts.
nodeGroup := router.Group("/node")
{
nodeGroup.GET("/info", nodeService.handleNodeInfo)
nodeGroup.POST("/init", nodeService.handleNodeInit)
}
// Peer management endpoints
// router.Group("/peers").POST("", nodeService.handleAddPeer) registers a peer like 10.0.0.2:9090.
peerGroup := router.Group("/peers")
{
peerGroup.GET("", nodeService.handleListPeers)
@ -66,7 +66,7 @@ func (nodeService *NodeService) SetupRoutes(router *gin.RouterGroup) {
peerGroup.POST("/:id/connect", nodeService.handleConnectPeer)
peerGroup.POST("/:id/disconnect", nodeService.handleDisconnectPeer)
// Allowlist management
// router.Group("/peers/auth/allowlist").POST("", nodeService.handleAddToAllowlist) accepts keys like ed25519:abc...
peerGroup.GET("/auth/mode", nodeService.handleGetAuthMode)
peerGroup.PUT("/auth/mode", nodeService.handleSetAuthMode)
peerGroup.GET("/auth/allowlist", nodeService.handleListAllowlist)
@ -74,7 +74,7 @@ func (nodeService *NodeService) SetupRoutes(router *gin.RouterGroup) {
peerGroup.DELETE("/auth/allowlist/:key", nodeService.handleRemoveFromAllowlist)
}
// Remote operations endpoints
// router.Group("/remote").POST("/:peerId/start", nodeService.handleRemoteStart) starts a miner on a peer like peer-123.
remoteGroup := router.Group("/remote")
{
remoteGroup.GET("/stats", nodeService.handleRemoteStats)
@ -110,7 +110,7 @@ type NodeInfoResponse struct {
// @Produce json
// @Success 200 {object} NodeInfoResponse
// @Router /node/info [get]
func (nodeService *NodeService) handleNodeInfo(c *gin.Context) {
func (nodeService *NodeService) handleNodeInfo(requestContext *gin.Context) {
response := NodeInfoResponse{
HasIdentity: nodeService.nodeManager.HasIdentity(),
RegisteredPeers: nodeService.peerRegistry.Count(),
@ -121,7 +121,7 @@ func (nodeService *NodeService) handleNodeInfo(c *gin.Context) {
response.Identity = nodeService.nodeManager.GetIdentity()
}
c.JSON(http.StatusOK, response)
requestContext.JSON(http.StatusOK, response)
}
// POST /node/init {"name": "my-node", "role": "worker"}
@ -139,15 +139,15 @@ type NodeInitRequest struct {
// @Param request body NodeInitRequest true "Node initialization parameters"
// @Success 200 {object} node.NodeIdentity
// @Router /node/init [post]
func (nodeService *NodeService) handleNodeInit(c *gin.Context) {
func (nodeService *NodeService) handleNodeInit(requestContext *gin.Context) {
var request NodeInitRequest
if err := c.ShouldBindJSON(&request); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
if err := requestContext.ShouldBindJSON(&request); err != nil {
requestContext.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if nodeService.nodeManager.HasIdentity() {
c.JSON(http.StatusConflict, gin.H{"error": "node identity already exists"})
requestContext.JSON(http.StatusConflict, gin.H{"error": "node identity already exists"})
return
}
@ -160,16 +160,16 @@ func (nodeService *NodeService) handleNodeInit(c *gin.Context) {
case "dual", "":
role = node.RoleDual
default:
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid role"})
requestContext.JSON(http.StatusBadRequest, gin.H{"error": "invalid role"})
return
}
if err := nodeService.nodeManager.GenerateIdentity(request.Name, role); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
requestContext.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, nodeService.nodeManager.GetIdentity())
requestContext.JSON(http.StatusOK, nodeService.nodeManager.GetIdentity())
}
// handleListPeers godoc
@ -179,9 +179,9 @@ func (nodeService *NodeService) handleNodeInit(c *gin.Context) {
// @Produce json
// @Success 200 {array} node.Peer
// @Router /peers [get]
func (nodeService *NodeService) handleListPeers(c *gin.Context) {
func (nodeService *NodeService) handleListPeers(requestContext *gin.Context) {
peers := nodeService.peerRegistry.ListPeers()
c.JSON(http.StatusOK, peers)
requestContext.JSON(http.StatusOK, peers)
}
// POST /peers {"address": "10.0.0.2:9090", "name": "worker-1"}
@ -199,10 +199,10 @@ type AddPeerRequest struct {
// @Param request body AddPeerRequest true "Peer information"
// @Success 201 {object} node.Peer
// @Router /peers [post]
func (nodeService *NodeService) handleAddPeer(c *gin.Context) {
func (nodeService *NodeService) handleAddPeer(requestContext *gin.Context) {
var request AddPeerRequest
if err := c.ShouldBindJSON(&request); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
if err := requestContext.ShouldBindJSON(&request); err != nil {
requestContext.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
@ -215,11 +215,11 @@ func (nodeService *NodeService) handleAddPeer(c *gin.Context) {
}
if err := nodeService.peerRegistry.AddPeer(peer); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
requestContext.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, peer)
requestContext.JSON(http.StatusCreated, peer)
}
// handleGetPeer godoc
@ -230,14 +230,14 @@ func (nodeService *NodeService) handleAddPeer(c *gin.Context) {
// @Param id path string true "Peer ID"
// @Success 200 {object} node.Peer
// @Router /peers/{id} [get]
func (nodeService *NodeService) handleGetPeer(c *gin.Context) {
peerID := c.Param("id")
func (nodeService *NodeService) handleGetPeer(requestContext *gin.Context) {
peerID := requestContext.Param("id")
peer := nodeService.peerRegistry.GetPeer(peerID)
if peer == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "peer not found"})
requestContext.JSON(http.StatusNotFound, gin.H{"error": "peer not found"})
return
}
c.JSON(http.StatusOK, peer)
requestContext.JSON(http.StatusOK, peer)
}
// handleRemovePeer godoc
@ -248,13 +248,13 @@ func (nodeService *NodeService) handleGetPeer(c *gin.Context) {
// @Param id path string true "Peer ID"
// @Success 200 {object} map[string]string
// @Router /peers/{id} [delete]
func (nodeService *NodeService) handleRemovePeer(c *gin.Context) {
peerID := c.Param("id")
func (nodeService *NodeService) handleRemovePeer(requestContext *gin.Context) {
peerID := requestContext.Param("id")
if err := nodeService.peerRegistry.RemovePeer(peerID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
requestContext.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"status": "peer removed"})
requestContext.JSON(http.StatusOK, gin.H{"status": "peer removed"})
}
// handlePingPeer godoc
@ -266,18 +266,18 @@ func (nodeService *NodeService) handleRemovePeer(c *gin.Context) {
// @Success 200 {object} map[string]float64
// @Failure 404 {object} APIError "Peer not found"
// @Router /peers/{id}/ping [post]
func (nodeService *NodeService) handlePingPeer(c *gin.Context) {
peerID := c.Param("id")
func (nodeService *NodeService) handlePingPeer(requestContext *gin.Context) {
peerID := requestContext.Param("id")
rtt, err := nodeService.controller.PingPeer(peerID)
if err != nil {
if containsStr(err.Error(), "not found") || containsStr(err.Error(), "not connected") {
respondWithError(c, http.StatusNotFound, "PEER_NOT_FOUND", "peer not found or not connected", err.Error())
respondWithError(requestContext, http.StatusNotFound, "PEER_NOT_FOUND", "peer not found or not connected", err.Error())
return
}
respondWithError(c, http.StatusInternalServerError, ErrCodeInternalError, "ping failed", err.Error())
respondWithError(requestContext, http.StatusInternalServerError, ErrCodeInternalError, "ping failed", err.Error())
return
}
c.JSON(http.StatusOK, gin.H{"rtt_ms": rtt})
requestContext.JSON(http.StatusOK, gin.H{"rtt_ms": rtt})
}
// handleConnectPeer godoc
@ -289,17 +289,17 @@ func (nodeService *NodeService) handlePingPeer(c *gin.Context) {
// @Success 200 {object} map[string]string
// @Failure 404 {object} APIError "Peer not found"
// @Router /peers/{id}/connect [post]
func (nodeService *NodeService) handleConnectPeer(c *gin.Context) {
peerID := c.Param("id")
func (nodeService *NodeService) handleConnectPeer(requestContext *gin.Context) {
peerID := requestContext.Param("id")
if err := nodeService.controller.ConnectToPeer(peerID); err != nil {
if containsStr(err.Error(), "not found") {
respondWithError(c, http.StatusNotFound, "PEER_NOT_FOUND", "peer not found", err.Error())
respondWithError(requestContext, http.StatusNotFound, "PEER_NOT_FOUND", "peer not found", err.Error())
return
}
respondWithError(c, http.StatusInternalServerError, ErrCodeConnectionFailed, "connection failed", err.Error())
respondWithError(requestContext, http.StatusInternalServerError, ErrCodeConnectionFailed, "connection failed", err.Error())
return
}
c.JSON(http.StatusOK, gin.H{"status": "connected"})
requestContext.JSON(http.StatusOK, gin.H{"status": "connected"})
}
// handleDisconnectPeer godoc
@ -310,18 +310,18 @@ func (nodeService *NodeService) handleConnectPeer(c *gin.Context) {
// @Param id path string true "Peer ID"
// @Success 200 {object} map[string]string
// @Router /peers/{id}/disconnect [post]
func (nodeService *NodeService) handleDisconnectPeer(c *gin.Context) {
peerID := c.Param("id")
func (nodeService *NodeService) handleDisconnectPeer(requestContext *gin.Context) {
peerID := requestContext.Param("id")
if err := nodeService.controller.DisconnectFromPeer(peerID); err != nil {
// Make disconnect idempotent - if peer not connected, still return success
if containsStr(err.Error(), "not connected") {
c.JSON(http.StatusOK, gin.H{"status": "disconnected"})
requestContext.JSON(http.StatusOK, gin.H{"status": "disconnected"})
return
}
respondWithError(c, http.StatusInternalServerError, ErrCodeInternalError, "disconnect failed", err.Error())
respondWithError(requestContext, http.StatusInternalServerError, ErrCodeInternalError, "disconnect failed", err.Error())
return
}
c.JSON(http.StatusOK, gin.H{"status": "disconnected"})
requestContext.JSON(http.StatusOK, gin.H{"status": "disconnected"})
}
// handleRemoteStats godoc
@ -331,9 +331,9 @@ func (nodeService *NodeService) handleDisconnectPeer(c *gin.Context) {
// @Produce json
// @Success 200 {object} map[string]node.StatsPayload
// @Router /remote/stats [get]
func (nodeService *NodeService) handleRemoteStats(c *gin.Context) {
func (nodeService *NodeService) handleRemoteStats(requestContext *gin.Context) {
stats := nodeService.controller.GetAllStats()
c.JSON(http.StatusOK, stats)
requestContext.JSON(http.StatusOK, stats)
}
// handlePeerStats godoc
@ -344,14 +344,14 @@ func (nodeService *NodeService) handleRemoteStats(c *gin.Context) {
// @Param peerId path string true "Peer ID"
// @Success 200 {object} node.StatsPayload
// @Router /remote/{peerId}/stats [get]
func (nodeService *NodeService) handlePeerStats(c *gin.Context) {
peerID := c.Param("peerId")
func (nodeService *NodeService) handlePeerStats(requestContext *gin.Context) {
peerID := requestContext.Param("peerId")
stats, err := nodeService.controller.GetRemoteStats(peerID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
requestContext.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, stats)
requestContext.JSON(http.StatusOK, stats)
}
// POST /remote/{peerId}/start {"minerType": "xmrig", "profileId": "abc123"}
@ -371,19 +371,19 @@ type RemoteStartRequest struct {
// @Param request body RemoteStartRequest true "Start parameters"
// @Success 200 {object} map[string]string
// @Router /remote/{peerId}/start [post]
func (nodeService *NodeService) handleRemoteStart(c *gin.Context) {
peerID := c.Param("peerId")
func (nodeService *NodeService) handleRemoteStart(requestContext *gin.Context) {
peerID := requestContext.Param("peerId")
var request RemoteStartRequest
if err := c.ShouldBindJSON(&request); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
if err := requestContext.ShouldBindJSON(&request); err != nil {
requestContext.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := nodeService.controller.StartRemoteMiner(peerID, request.MinerType, request.ProfileID, request.Config); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
requestContext.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"status": "miner started"})
requestContext.JSON(http.StatusOK, gin.H{"status": "miner started"})
}
// POST /remote/{peerId}/stop {"minerName": "xmrig-main"}
@ -401,19 +401,19 @@ type RemoteStopRequest struct {
// @Param request body RemoteStopRequest true "Stop parameters"
// @Success 200 {object} map[string]string
// @Router /remote/{peerId}/stop [post]
func (nodeService *NodeService) handleRemoteStop(c *gin.Context) {
peerID := c.Param("peerId")
func (nodeService *NodeService) handleRemoteStop(requestContext *gin.Context) {
peerID := requestContext.Param("peerId")
var request RemoteStopRequest
if err := c.ShouldBindJSON(&request); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
if err := requestContext.ShouldBindJSON(&request); err != nil {
requestContext.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := nodeService.controller.StopRemoteMiner(peerID, request.MinerName); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
requestContext.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"status": "miner stopped"})
requestContext.JSON(http.StatusOK, gin.H{"status": "miner stopped"})
}
// handleRemoteLogs godoc
@ -426,12 +426,12 @@ func (nodeService *NodeService) handleRemoteStop(c *gin.Context) {
// @Param lines query int false "Number of lines (max 10000)" default(100)
// @Success 200 {array} string
// @Router /remote/{peerId}/logs/{miner} [get]
func (nodeService *NodeService) handleRemoteLogs(c *gin.Context) {
peerID := c.Param("peerId")
minerName := c.Param("miner")
func (nodeService *NodeService) handleRemoteLogs(requestContext *gin.Context) {
peerID := requestContext.Param("peerId")
minerName := requestContext.Param("miner")
lines := 100
const maxLines = 10000 // Prevent resource exhaustion
if linesParam := c.Query("lines"); linesParam != "" {
if linesParam := requestContext.Query("lines"); linesParam != "" {
if parsed, err := strconv.Atoi(linesParam); err == nil && parsed > 0 {
lines = parsed
if lines > maxLines {
@ -442,10 +442,10 @@ func (nodeService *NodeService) handleRemoteLogs(c *gin.Context) {
logs, err := nodeService.controller.GetRemoteLogs(peerID, minerName, lines)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
requestContext.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, logs)
requestContext.JSON(http.StatusOK, logs)
}
// GET /peers/auth/mode → {"mode": "open"} or {"mode": "allowlist"}
@ -460,13 +460,13 @@ type AuthModeResponse struct {
// @Produce json
// @Success 200 {object} AuthModeResponse
// @Router /peers/auth/mode [get]
func (nodeService *NodeService) handleGetAuthMode(c *gin.Context) {
func (nodeService *NodeService) handleGetAuthMode(requestContext *gin.Context) {
mode := nodeService.peerRegistry.GetAuthMode()
modeStr := "open"
if mode == node.PeerAuthAllowlist {
modeStr = "allowlist"
}
c.JSON(http.StatusOK, AuthModeResponse{Mode: modeStr})
requestContext.JSON(http.StatusOK, AuthModeResponse{Mode: modeStr})
}
// PUT /peers/auth/mode {"mode": "allowlist"} // or "open"
@ -484,10 +484,10 @@ type SetAuthModeRequest struct {
// @Success 200 {object} AuthModeResponse
// @Failure 400 {object} APIError "Invalid mode"
// @Router /peers/auth/mode [put]
func (nodeService *NodeService) handleSetAuthMode(c *gin.Context) {
func (nodeService *NodeService) handleSetAuthMode(requestContext *gin.Context) {
var request SetAuthModeRequest
if err := c.ShouldBindJSON(&request); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
if err := requestContext.ShouldBindJSON(&request); err != nil {
requestContext.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
@ -498,12 +498,12 @@ func (nodeService *NodeService) handleSetAuthMode(c *gin.Context) {
case "allowlist":
mode = node.PeerAuthAllowlist
default:
respondWithError(c, http.StatusBadRequest, "INVALID_MODE", "mode must be 'open' or 'allowlist'", "")
respondWithError(requestContext, http.StatusBadRequest, "INVALID_MODE", "mode must be 'open' or 'allowlist'", "")
return
}
nodeService.peerRegistry.SetAuthMode(mode)
c.JSON(http.StatusOK, AuthModeResponse{Mode: request.Mode})
requestContext.JSON(http.StatusOK, AuthModeResponse{Mode: request.Mode})
}
// GET /peers/auth/allowlist → {"publicKeys": ["ed25519:abc...", "ed25519:def..."]}
@ -518,9 +518,9 @@ type AllowlistResponse struct {
// @Produce json
// @Success 200 {object} AllowlistResponse
// @Router /peers/auth/allowlist [get]
func (nodeService *NodeService) handleListAllowlist(c *gin.Context) {
func (nodeService *NodeService) handleListAllowlist(requestContext *gin.Context) {
keys := nodeService.peerRegistry.ListAllowedPublicKeys()
c.JSON(http.StatusOK, AllowlistResponse{PublicKeys: keys})
requestContext.JSON(http.StatusOK, AllowlistResponse{PublicKeys: keys})
}
// POST /peers/auth/allowlist {"publicKey": "ed25519:abc123..."}
@ -538,20 +538,20 @@ type AddAllowlistRequest struct {
// @Success 201 {object} map[string]string
// @Failure 400 {object} APIError "Invalid request"
// @Router /peers/auth/allowlist [post]
func (nodeService *NodeService) handleAddToAllowlist(c *gin.Context) {
func (nodeService *NodeService) handleAddToAllowlist(requestContext *gin.Context) {
var request AddAllowlistRequest
if err := c.ShouldBindJSON(&request); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
if err := requestContext.ShouldBindJSON(&request); err != nil {
requestContext.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if len(request.PublicKey) < 16 {
respondWithError(c, http.StatusBadRequest, "INVALID_KEY", "public key too short", "")
respondWithError(requestContext, http.StatusBadRequest, "INVALID_KEY", "public key too short", "")
return
}
nodeService.peerRegistry.AllowPublicKey(request.PublicKey)
c.JSON(http.StatusCreated, gin.H{"status": "added"})
requestContext.JSON(http.StatusCreated, gin.H{"status": "added"})
}
// handleRemoveFromAllowlist godoc
@ -562,13 +562,13 @@ func (nodeService *NodeService) handleAddToAllowlist(c *gin.Context) {
// @Param key path string true "Public key to remove (URL-encoded)"
// @Success 200 {object} map[string]string
// @Router /peers/auth/allowlist/{key} [delete]
func (nodeService *NodeService) handleRemoveFromAllowlist(c *gin.Context) {
key := c.Param("key")
func (nodeService *NodeService) handleRemoveFromAllowlist(requestContext *gin.Context) {
key := requestContext.Param("key")
if key == "" {
respondWithError(c, http.StatusBadRequest, "MISSING_KEY", "public key required", "")
respondWithError(requestContext, http.StatusBadRequest, "MISSING_KEY", "public key required", "")
return
}
nodeService.peerRegistry.RevokePublicKey(key)
c.JSON(http.StatusOK, gin.H{"status": "removed"})
requestContext.JSON(http.StatusOK, gin.H{"status": "removed"})
}

View file

@ -80,8 +80,8 @@ func (limiter *RateLimiter) Stop() {
// router.Use(limiter.Middleware()) // install before route handlers
func (limiter *RateLimiter) Middleware() gin.HandlerFunc {
return func(c *gin.Context) {
clientAddress := c.ClientIP()
return func(requestContext *gin.Context) {
clientAddress := requestContext.ClientIP()
limiter.mutex.Lock()
client, exists := limiter.clients[clientAddress]
@ -101,15 +101,15 @@ func (limiter *RateLimiter) Middleware() gin.HandlerFunc {
if client.tokens < 1 {
limiter.mutex.Unlock()
respondWithError(c, http.StatusTooManyRequests, "RATE_LIMITED",
respondWithError(requestContext, http.StatusTooManyRequests, "RATE_LIMITED",
"too many requests", "rate limit exceeded")
c.Abort()
requestContext.Abort()
return
}
client.tokens--
limiter.mutex.Unlock()
c.Next()
requestContext.Next()
}
}

View file

@ -279,60 +279,60 @@ const (
// router.Use(cacheMiddleware()) serves GET /miners/available with Cache-Control: public, max-age=300.
func cacheMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
return func(requestContext *gin.Context) {
// Only cache GET requests like /api/v1/mining/info.
if c.Request.Method != http.MethodGet {
c.Header("Cache-Control", CacheNoStore)
c.Next()
if requestContext.Request.Method != http.MethodGet {
requestContext.Header("Cache-Control", CacheNoStore)
requestContext.Next()
return
}
path := c.Request.URL.Path
path := requestContext.Request.URL.Path
// Cache GET /api/v1/mining/miners/available for 5 minutes.
switch {
case strings.HasSuffix(path, "/available"):
c.Header("Cache-Control", CachePublic5Min)
requestContext.Header("Cache-Control", CachePublic5Min)
case strings.HasSuffix(path, "/info"):
// Cache GET /api/v1/mining/info for 1 minute.
c.Header("Cache-Control", CachePublic1Min)
requestContext.Header("Cache-Control", CachePublic1Min)
case strings.Contains(path, "/swagger"):
// Cache GET /api/v1/mining/swagger/index.html for 5 minutes.
c.Header("Cache-Control", CachePublic5Min)
requestContext.Header("Cache-Control", CachePublic5Min)
default:
// Keep dynamic requests like /api/v1/mining/miners/xmrig live.
c.Header("Cache-Control", CacheNoCache)
requestContext.Header("Cache-Control", CacheNoCache)
}
c.Next()
requestContext.Next()
}
}
// router.Use(requestTimeoutMiddleware(30 * time.Second)) aborts GET /history/miners/xmrig after 30 seconds.
func requestTimeoutMiddleware(timeout time.Duration) gin.HandlerFunc {
return func(c *gin.Context) {
return func(requestContext *gin.Context) {
// Skip timeout for WebSocket upgrades like /ws/events.
if c.GetHeader("Upgrade") == "websocket" {
c.Next()
if requestContext.GetHeader("Upgrade") == "websocket" {
requestContext.Next()
return
}
if strings.HasSuffix(c.Request.URL.Path, "/events") {
c.Next()
if strings.HasSuffix(requestContext.Request.URL.Path, "/events") {
requestContext.Next()
return
}
// Create a request-scoped timeout for requests like GET /api/v1/mining/history/xmrig.
ctx, cancel := context.WithTimeout(c.Request.Context(), timeout)
ctx, cancel := context.WithTimeout(requestContext.Request.Context(), timeout)
defer cancel()
// Replace the incoming request context with the timed one.
c.Request = c.Request.WithContext(ctx)
requestContext.Request = requestContext.Request.WithContext(ctx)
var responded int32
done := make(chan struct{})
go func() {
c.Next()
requestContext.Next()
atomic.StoreInt32(&responded, 1)
close(done)
}()
@ -343,15 +343,15 @@ func requestTimeoutMiddleware(timeout time.Duration) gin.HandlerFunc {
case <-ctx.Done():
// Timeout occurred; only respond if the handler has not already written a response.
if atomic.CompareAndSwapInt32(&responded, 0, 1) {
c.Abort()
respondWithError(c, http.StatusGatewayTimeout, ErrCodeTimeout,
requestContext.Abort()
respondWithError(requestContext, http.StatusGatewayTimeout, ErrCodeTimeout,
"Request timed out", "Request exceeded "+timeout.String()+" timeout")
}
}
}
}
// conn, err := wsUpgrader.Upgrade(c.Writer, c.Request, nil) upgrades GET /ws/events to WebSocket.
// conn, err := wsUpgrader.Upgrade(requestContext.Writer, requestContext.Request, nil) upgrades GET /ws/events to WebSocket.
var wsUpgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
@ -415,8 +415,8 @@ func NewService(manager ManagerInterface, listenAddress string, displayAddress s
if len(miners) == 0 {
return nil
}
// state := []map[string]interface{}{{"name": "xmrig-rx_0", "status": "running"}}
state := make([]map[string]interface{}, 0, len(miners))
// minerStates := []map[string]interface{}{{"name": "xmrig-rx_0", "status": "running"}}
minerStates := make([]map[string]interface{}, 0, len(miners))
for _, miner := range miners {
stats, _ := miner.GetStats(context.Background())
minerState := map[string]interface{}{
@ -429,10 +429,10 @@ func NewService(manager ManagerInterface, listenAddress string, displayAddress s
minerState["rejected"] = stats.Rejected
minerState["uptime"] = stats.Uptime
}
state = append(state, minerState)
minerStates = append(minerStates, minerState)
}
return map[string]interface{}{
"miners": state,
"miners": minerStates,
}
})
@ -502,10 +502,10 @@ func (service *Service) InitRouter() {
// service.Router.Use(contentTypeValidationMiddleware()) // rejects POST /api/v1/mining/profiles without application/json.
service.Router.Use(contentTypeValidationMiddleware())
// c.Request.Body = http.MaxBytesReader(..., 1<<20) // caps bodies for requests like POST /api/v1/mining/profiles.
service.Router.Use(func(c *gin.Context) {
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 1<<20) // 1MB
c.Next()
// requestContext.Request.Body = http.MaxBytesReader(..., 1<<20) // caps bodies for requests like POST /api/v1/mining/profiles.
service.Router.Use(func(requestContext *gin.Context) {
requestContext.Request.Body = http.MaxBytesReader(requestContext.Writer, requestContext.Request.Body, 1<<20) // 1MB
requestContext.Next()
})
// service.Router.Use(csrfMiddleware()) // allows API clients with Authorization or X-Requested-With headers.
@ -678,7 +678,7 @@ func (service *Service) SetupRoutes() {
logging.Info("MCP server enabled", logging.Fields{"endpoint": service.APIBasePath + "/mcp"})
}
// c.JSON(http.StatusOK, HealthResponse{Status: "healthy", Components: map[string]string{"db": "ok"}})
// requestContext.JSON(http.StatusOK, HealthResponse{Status: "healthy", Components: map[string]string{"db": "ok"}})
type HealthResponse struct {
Status string `json:"status"`
Components map[string]string `json:"components,omitempty"`
@ -691,8 +691,8 @@ type HealthResponse struct {
// @Produce json
// @Success 200 {object} HealthResponse
// @Router /health [get]
func (service *Service) handleHealth(c *gin.Context) {
c.JSON(http.StatusOK, HealthResponse{
func (service *Service) handleHealth(requestContext *gin.Context) {
requestContext.JSON(http.StatusOK, HealthResponse{
Status: "healthy",
})
}
@ -705,7 +705,7 @@ func (service *Service) handleHealth(c *gin.Context) {
// @Success 200 {object} HealthResponse
// @Success 503 {object} HealthResponse
// @Router /ready [get]
func (service *Service) handleReady(c *gin.Context) {
func (service *Service) handleReady(requestContext *gin.Context) {
components := make(map[string]string)
allReady := true
@ -747,7 +747,7 @@ func (service *Service) handleReady(c *gin.Context) {
httpStatus = http.StatusServiceUnavailable
}
c.JSON(httpStatus, HealthResponse{
requestContext.JSON(httpStatus, HealthResponse{
Status: status,
Components: components,
})
@ -761,13 +761,13 @@ func (service *Service) handleReady(c *gin.Context) {
// @Success 200 {object} SystemInfo
// @Failure 500 {object} map[string]string "Internal server error"
// @Router /info [get]
func (service *Service) handleGetInfo(c *gin.Context) {
func (service *Service) handleGetInfo(requestContext *gin.Context) {
systemInfo, err := service.updateInstallationCache()
if err != nil {
respondWithMiningError(c, ErrInternal("failed to get system info").WithCause(err))
respondWithMiningError(requestContext, ErrInternal("failed to get system info").WithCause(err))
return
}
c.JSON(http.StatusOK, systemInfo)
requestContext.JSON(http.StatusOK, systemInfo)
}
// systemInfo, err := service.updateInstallationCache()
@ -828,13 +828,13 @@ func (service *Service) updateInstallationCache() (*SystemInfo, error) {
// @Produce json
// @Success 200 {object} SystemInfo
// @Router /doctor [post]
func (service *Service) handleDoctor(c *gin.Context) {
func (service *Service) handleDoctor(requestContext *gin.Context) {
systemInfo, err := service.updateInstallationCache()
if err != nil {
respondWithMiningError(c, ErrInternal("failed to update cache").WithCause(err))
respondWithMiningError(requestContext, ErrInternal("failed to update cache").WithCause(err))
return
}
c.JSON(http.StatusOK, systemInfo)
requestContext.JSON(http.StatusOK, systemInfo)
}
// handleUpdateCheck godoc
@ -844,7 +844,7 @@ func (service *Service) handleDoctor(c *gin.Context) {
// @Produce json
// @Success 200 {object} map[string]string
// @Router /update [post]
func (service *Service) handleUpdateCheck(c *gin.Context) {
func (service *Service) handleUpdateCheck(requestContext *gin.Context) {
updates := make(map[string]string)
for _, availableMiner := range service.Manager.ListAvailableMiners() {
miner, err := CreateMiner(availableMiner.Name)
@ -878,11 +878,11 @@ func (service *Service) handleUpdateCheck(c *gin.Context) {
}
if len(updates) == 0 {
c.JSON(http.StatusOK, gin.H{"status": "All miners are up to date."})
requestContext.JSON(http.StatusOK, gin.H{"status": "All miners are up to date."})
return
}
c.JSON(http.StatusOK, gin.H{"updates_available": updates})
requestContext.JSON(http.StatusOK, gin.H{"updates_available": updates})
}
// handleUninstallMiner godoc
@ -893,16 +893,16 @@ func (service *Service) handleUpdateCheck(c *gin.Context) {
// @Param miner_type path string true "Miner Type to uninstall"
// @Success 200 {object} map[string]string
// @Router /miners/{miner_type}/uninstall [delete]
func (service *Service) handleUninstallMiner(c *gin.Context) {
minerType := c.Param("miner_name")
if err := service.Manager.UninstallMiner(c.Request.Context(), minerType); err != nil {
respondWithMiningError(c, ErrInternal("failed to uninstall miner").WithCause(err))
func (service *Service) handleUninstallMiner(requestContext *gin.Context) {
minerType := requestContext.Param("miner_name")
if err := service.Manager.UninstallMiner(requestContext.Request.Context(), minerType); err != nil {
respondWithMiningError(requestContext, ErrInternal("failed to uninstall miner").WithCause(err))
return
}
if _, err := service.updateInstallationCache(); err != nil {
logging.Warn("failed to update cache after uninstall", logging.Fields{"error": err})
}
c.JSON(http.StatusOK, gin.H{"status": minerType + " uninstalled successfully."})
requestContext.JSON(http.StatusOK, gin.H{"status": minerType + " uninstalled successfully."})
}
// handleListMiners godoc
@ -912,9 +912,9 @@ func (service *Service) handleUninstallMiner(c *gin.Context) {
// @Produce json
// @Success 200 {array} XMRigMiner
// @Router /miners [get]
func (service *Service) handleListMiners(c *gin.Context) {
func (service *Service) handleListMiners(requestContext *gin.Context) {
miners := service.Manager.ListMiners()
c.JSON(http.StatusOK, miners)
requestContext.JSON(http.StatusOK, miners)
}
// handleListAvailableMiners godoc
@ -924,9 +924,9 @@ func (service *Service) handleListMiners(c *gin.Context) {
// @Produce json
// @Success 200 {array} AvailableMiner
// @Router /miners/available [get]
func (service *Service) handleListAvailableMiners(c *gin.Context) {
func (service *Service) handleListAvailableMiners(requestContext *gin.Context) {
miners := service.Manager.ListAvailableMiners()
c.JSON(http.StatusOK, miners)
requestContext.JSON(http.StatusOK, miners)
}
// handleInstallMiner godoc
@ -937,16 +937,16 @@ func (service *Service) handleListAvailableMiners(c *gin.Context) {
// @Param miner_type path string true "Miner Type to install/update"
// @Success 200 {object} map[string]string
// @Router /miners/{miner_type}/install [post]
func (service *Service) handleInstallMiner(c *gin.Context) {
minerType := c.Param("miner_name")
func (service *Service) handleInstallMiner(requestContext *gin.Context) {
minerType := requestContext.Param("miner_name")
miner, err := CreateMiner(minerType)
if err != nil {
respondWithMiningError(c, ErrUnsupportedMiner(minerType))
respondWithMiningError(requestContext, ErrUnsupportedMiner(minerType))
return
}
if err := miner.Install(); err != nil {
respondWithMiningError(c, ErrInstallFailed(minerType).WithCause(err))
respondWithMiningError(requestContext, ErrInstallFailed(minerType).WithCause(err))
return
}
@ -956,11 +956,11 @@ func (service *Service) handleInstallMiner(c *gin.Context) {
details, err := miner.CheckInstallation()
if err != nil {
respondWithMiningError(c, ErrInternal("failed to verify installation").WithCause(err))
respondWithMiningError(requestContext, ErrInternal("failed to verify installation").WithCause(err))
return
}
c.JSON(http.StatusOK, gin.H{"status": "installed", "version": details.Version, "path": details.Path})
requestContext.JSON(http.StatusOK, gin.H{"status": "installed", "version": details.Version, "path": details.Path})
}
// handleStartMinerWithProfile godoc
@ -971,32 +971,32 @@ func (service *Service) handleInstallMiner(c *gin.Context) {
// @Param id path string true "Profile ID"
// @Success 200 {object} XMRigMiner
// @Router /profiles/{id}/start [post]
func (service *Service) handleStartMinerWithProfile(c *gin.Context) {
profileID := c.Param("id")
func (service *Service) handleStartMinerWithProfile(requestContext *gin.Context) {
profileID := requestContext.Param("id")
profile, exists := service.ProfileManager.GetProfile(profileID)
if !exists {
respondWithMiningError(c, ErrProfileNotFound(profileID))
respondWithMiningError(requestContext, ErrProfileNotFound(profileID))
return
}
var config Config
if err := json.Unmarshal(profile.Config, &config); err != nil {
respondWithMiningError(c, ErrInvalidConfig("failed to parse profile config").WithCause(err))
var minerConfig Config
if err := json.Unmarshal(profile.Config, &minerConfig); err != nil {
respondWithMiningError(requestContext, ErrInvalidConfig("failed to parse profile config").WithCause(err))
return
}
// Validate config from profile to prevent shell injection and other issues
if err := config.Validate(); err != nil {
respondWithMiningError(c, ErrInvalidConfig("profile config validation failed").WithCause(err))
if err := minerConfig.Validate(); err != nil {
respondWithMiningError(requestContext, ErrInvalidConfig("profile config validation failed").WithCause(err))
return
}
miner, err := service.Manager.StartMiner(c.Request.Context(), profile.MinerType, &config)
miner, err := service.Manager.StartMiner(requestContext.Request.Context(), profile.MinerType, &minerConfig)
if err != nil {
respondWithMiningError(c, ErrStartFailed(profile.Name).WithCause(err))
respondWithMiningError(requestContext, ErrStartFailed(profile.Name).WithCause(err))
return
}
c.JSON(http.StatusOK, miner)
requestContext.JSON(http.StatusOK, miner)
}
// handleStopMiner godoc
@ -1007,13 +1007,13 @@ func (service *Service) handleStartMinerWithProfile(c *gin.Context) {
// @Param miner_name path string true "Miner Name"
// @Success 200 {object} map[string]string
// @Router /miners/{miner_name} [delete]
func (service *Service) handleStopMiner(c *gin.Context) {
minerName := c.Param("miner_name")
if err := service.Manager.StopMiner(c.Request.Context(), minerName); err != nil {
respondWithMiningError(c, ErrStopFailed(minerName).WithCause(err))
func (service *Service) handleStopMiner(requestContext *gin.Context) {
minerName := requestContext.Param("miner_name")
if err := service.Manager.StopMiner(requestContext.Request.Context(), minerName); err != nil {
respondWithMiningError(requestContext, ErrStopFailed(minerName).WithCause(err))
return
}
c.JSON(http.StatusOK, gin.H{"status": "stopped"})
requestContext.JSON(http.StatusOK, gin.H{"status": "stopped"})
}
// handleGetMinerStats godoc
@ -1024,19 +1024,19 @@ func (service *Service) handleStopMiner(c *gin.Context) {
// @Param miner_name path string true "Miner Name"
// @Success 200 {object} PerformanceMetrics
// @Router /miners/{miner_name}/stats [get]
func (service *Service) handleGetMinerStats(c *gin.Context) {
minerName := c.Param("miner_name")
func (service *Service) handleGetMinerStats(requestContext *gin.Context) {
minerName := requestContext.Param("miner_name")
miner, err := service.Manager.GetMiner(minerName)
if err != nil {
respondWithMiningError(c, ErrMinerNotFound(minerName).WithCause(err))
respondWithMiningError(requestContext, ErrMinerNotFound(minerName).WithCause(err))
return
}
stats, err := miner.GetStats(c.Request.Context())
stats, err := miner.GetStats(requestContext.Request.Context())
if err != nil {
respondWithMiningError(c, ErrInternal("failed to get miner stats").WithCause(err))
respondWithMiningError(requestContext, ErrInternal("failed to get miner stats").WithCause(err))
return
}
c.JSON(http.StatusOK, stats)
requestContext.JSON(http.StatusOK, stats)
}
// handleGetMinerHashrateHistory godoc
@ -1047,14 +1047,14 @@ func (service *Service) handleGetMinerStats(c *gin.Context) {
// @Param miner_name path string true "Miner Name"
// @Success 200 {array} HashratePoint
// @Router /miners/{miner_name}/hashrate-history [get]
func (service *Service) handleGetMinerHashrateHistory(c *gin.Context) {
minerName := c.Param("miner_name")
func (service *Service) handleGetMinerHashrateHistory(requestContext *gin.Context) {
minerName := requestContext.Param("miner_name")
history, err := service.Manager.GetMinerHashrateHistory(minerName)
if err != nil {
respondWithMiningError(c, ErrMinerNotFound(minerName).WithCause(err))
respondWithMiningError(requestContext, ErrMinerNotFound(minerName).WithCause(err))
return
}
c.JSON(http.StatusOK, history)
requestContext.JSON(http.StatusOK, history)
}
// handleGetMinerLogs godoc
@ -1065,11 +1065,11 @@ func (service *Service) handleGetMinerHashrateHistory(c *gin.Context) {
// @Param miner_name path string true "Miner Name"
// @Success 200 {array} string "Base64 encoded log lines"
// @Router /miners/{miner_name}/logs [get]
func (service *Service) handleGetMinerLogs(c *gin.Context) {
minerName := c.Param("miner_name")
func (service *Service) handleGetMinerLogs(requestContext *gin.Context) {
minerName := requestContext.Param("miner_name")
miner, err := service.Manager.GetMiner(minerName)
if err != nil {
respondWithMiningError(c, ErrMinerNotFound(minerName).WithCause(err))
respondWithMiningError(requestContext, ErrMinerNotFound(minerName).WithCause(err))
return
}
logs := miner.GetLogs()
@ -1078,10 +1078,10 @@ func (service *Service) handleGetMinerLogs(c *gin.Context) {
for i, line := range logs {
encodedLogs[i] = base64.StdEncoding.EncodeToString([]byte(line))
}
c.JSON(http.StatusOK, encodedLogs)
requestContext.JSON(http.StatusOK, encodedLogs)
}
// c.ShouldBindJSON(&StdinInput{Input: "h"}) // `h` prints hash rate and `p` pauses mining.
// requestContext.ShouldBindJSON(&StdinInput{Input: "h"}) // `h` prints hash rate and `p` pauses mining.
type StdinInput struct {
Input string `json:"input" binding:"required"`
}
@ -1098,26 +1098,26 @@ type StdinInput struct {
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Router /miners/{miner_name}/stdin [post]
func (service *Service) handleMinerStdin(c *gin.Context) {
minerName := c.Param("miner_name")
func (service *Service) handleMinerStdin(requestContext *gin.Context) {
minerName := requestContext.Param("miner_name")
miner, err := service.Manager.GetMiner(minerName)
if err != nil {
respondWithError(c, http.StatusNotFound, ErrCodeMinerNotFound, "miner not found", err.Error())
respondWithError(requestContext, http.StatusNotFound, ErrCodeMinerNotFound, "miner not found", err.Error())
return
}
var input StdinInput
if err := c.ShouldBindJSON(&input); err != nil {
respondWithMiningError(c, ErrInvalidConfig("invalid input format").WithCause(err))
if err := requestContext.ShouldBindJSON(&input); err != nil {
respondWithMiningError(requestContext, ErrInvalidConfig("invalid input format").WithCause(err))
return
}
if err := miner.WriteStdin(input.Input); err != nil {
respondWithMiningError(c, ErrInternal("failed to write to stdin").WithCause(err))
respondWithMiningError(requestContext, ErrInternal("failed to write to stdin").WithCause(err))
return
}
c.JSON(http.StatusOK, gin.H{"status": "sent", "input": input.Input})
requestContext.JSON(http.StatusOK, gin.H{"status": "sent", "input": input.Input})
}
// handleListProfiles godoc
@ -1127,9 +1127,9 @@ func (service *Service) handleMinerStdin(c *gin.Context) {
// @Produce json
// @Success 200 {array} MiningProfile
// @Router /profiles [get]
func (service *Service) handleListProfiles(c *gin.Context) {
func (service *Service) handleListProfiles(requestContext *gin.Context) {
profiles := service.ProfileManager.GetAllProfiles()
c.JSON(http.StatusOK, profiles)
requestContext.JSON(http.StatusOK, profiles)
}
// handleCreateProfile godoc
@ -1142,30 +1142,30 @@ func (service *Service) handleListProfiles(c *gin.Context) {
// @Success 201 {object} MiningProfile
// @Failure 400 {object} APIError "Invalid profile data"
// @Router /profiles [post]
func (service *Service) handleCreateProfile(c *gin.Context) {
func (service *Service) handleCreateProfile(requestContext *gin.Context) {
var profile MiningProfile
if err := c.ShouldBindJSON(&profile); err != nil {
respondWithError(c, http.StatusBadRequest, ErrCodeInvalidInput, "invalid profile data", err.Error())
if err := requestContext.ShouldBindJSON(&profile); err != nil {
respondWithError(requestContext, http.StatusBadRequest, ErrCodeInvalidInput, "invalid profile data", err.Error())
return
}
// Validate required fields
if profile.Name == "" {
respondWithError(c, http.StatusBadRequest, ErrCodeInvalidInput, "profile name is required", "")
respondWithError(requestContext, http.StatusBadRequest, ErrCodeInvalidInput, "profile name is required", "")
return
}
if profile.MinerType == "" {
respondWithError(c, http.StatusBadRequest, ErrCodeInvalidInput, "miner type is required", "")
respondWithError(requestContext, http.StatusBadRequest, ErrCodeInvalidInput, "miner type is required", "")
return
}
createdProfile, err := service.ProfileManager.CreateProfile(&profile)
if err != nil {
respondWithError(c, http.StatusInternalServerError, ErrCodeInternalError, "failed to create profile", err.Error())
respondWithError(requestContext, http.StatusInternalServerError, ErrCodeInternalError, "failed to create profile", err.Error())
return
}
c.JSON(http.StatusCreated, createdProfile)
requestContext.JSON(http.StatusCreated, createdProfile)
}
// handleGetProfile godoc
@ -1176,14 +1176,14 @@ func (service *Service) handleCreateProfile(c *gin.Context) {
// @Param id path string true "Profile ID"
// @Success 200 {object} MiningProfile
// @Router /profiles/{id} [get]
func (service *Service) handleGetProfile(c *gin.Context) {
profileID := c.Param("id")
func (service *Service) handleGetProfile(requestContext *gin.Context) {
profileID := requestContext.Param("id")
profile, exists := service.ProfileManager.GetProfile(profileID)
if !exists {
respondWithError(c, http.StatusNotFound, ErrCodeProfileNotFound, "profile not found", "")
respondWithError(requestContext, http.StatusNotFound, ErrCodeProfileNotFound, "profile not found", "")
return
}
c.JSON(http.StatusOK, profile)
requestContext.JSON(http.StatusOK, profile)
}
// handleUpdateProfile godoc
@ -1197,11 +1197,11 @@ func (service *Service) handleGetProfile(c *gin.Context) {
// @Success 200 {object} MiningProfile
// @Failure 404 {object} APIError "Profile not found"
// @Router /profiles/{id} [put]
func (service *Service) handleUpdateProfile(c *gin.Context) {
profileID := c.Param("id")
func (service *Service) handleUpdateProfile(requestContext *gin.Context) {
profileID := requestContext.Param("id")
var profile MiningProfile
if err := c.ShouldBindJSON(&profile); err != nil {
respondWithError(c, http.StatusBadRequest, ErrCodeInvalidInput, "invalid profile data", err.Error())
if err := requestContext.ShouldBindJSON(&profile); err != nil {
respondWithError(requestContext, http.StatusBadRequest, ErrCodeInvalidInput, "invalid profile data", err.Error())
return
}
profile.ID = profileID
@ -1209,13 +1209,13 @@ func (service *Service) handleUpdateProfile(c *gin.Context) {
if err := service.ProfileManager.UpdateProfile(&profile); err != nil {
// Check if error is "not found"
if strings.Contains(err.Error(), "not found") {
respondWithError(c, http.StatusNotFound, ErrCodeProfileNotFound, "profile not found", err.Error())
respondWithError(requestContext, http.StatusNotFound, ErrCodeProfileNotFound, "profile not found", err.Error())
return
}
respondWithError(c, http.StatusInternalServerError, ErrCodeInternalError, "failed to update profile", err.Error())
respondWithError(requestContext, http.StatusInternalServerError, ErrCodeInternalError, "failed to update profile", err.Error())
return
}
c.JSON(http.StatusOK, profile)
requestContext.JSON(http.StatusOK, profile)
}
// handleDeleteProfile godoc
@ -1226,18 +1226,18 @@ func (service *Service) handleUpdateProfile(c *gin.Context) {
// @Param id path string true "Profile ID"
// @Success 200 {object} map[string]string
// @Router /profiles/{id} [delete]
func (service *Service) handleDeleteProfile(c *gin.Context) {
profileID := c.Param("id")
func (service *Service) handleDeleteProfile(requestContext *gin.Context) {
profileID := requestContext.Param("id")
if err := service.ProfileManager.DeleteProfile(profileID); err != nil {
// Make DELETE idempotent - if profile doesn't exist, still return success
if strings.Contains(err.Error(), "not found") {
c.JSON(http.StatusOK, gin.H{"status": "profile deleted"})
requestContext.JSON(http.StatusOK, gin.H{"status": "profile deleted"})
return
}
respondWithError(c, http.StatusInternalServerError, ErrCodeInternalError, "failed to delete profile", err.Error())
respondWithError(requestContext, http.StatusInternalServerError, ErrCodeInternalError, "failed to delete profile", err.Error())
return
}
c.JSON(http.StatusOK, gin.H{"status": "profile deleted"})
requestContext.JSON(http.StatusOK, gin.H{"status": "profile deleted"})
}
// handleHistoryStatus godoc
@ -1247,15 +1247,15 @@ func (service *Service) handleDeleteProfile(c *gin.Context) {
// @Produce json
// @Success 200 {object} map[string]interface{}
// @Router /history/status [get]
func (service *Service) handleHistoryStatus(c *gin.Context) {
func (service *Service) handleHistoryStatus(requestContext *gin.Context) {
if manager, ok := service.Manager.(*Manager); ok {
c.JSON(http.StatusOK, gin.H{
requestContext.JSON(http.StatusOK, gin.H{
"enabled": manager.IsDatabaseEnabled(),
"retentionDays": manager.databaseRetention,
})
return
}
c.JSON(http.StatusOK, gin.H{"enabled": false, "error": "manager type not supported"})
requestContext.JSON(http.StatusOK, gin.H{"enabled": false, "error": "manager type not supported"})
}
// handleAllMinersHistoricalStats godoc
@ -1265,20 +1265,20 @@ func (service *Service) handleHistoryStatus(c *gin.Context) {
// @Produce json
// @Success 200 {array} database.HashrateStats
// @Router /history/miners [get]
func (service *Service) handleAllMinersHistoricalStats(c *gin.Context) {
func (service *Service) handleAllMinersHistoricalStats(requestContext *gin.Context) {
manager, ok := service.Manager.(*Manager)
if !ok {
respondWithMiningError(c, ErrInternal("manager type not supported"))
respondWithMiningError(requestContext, ErrInternal("manager type not supported"))
return
}
stats, err := manager.GetAllMinerHistoricalStats()
if err != nil {
respondWithMiningError(c, ErrDatabaseError("get historical stats").WithCause(err))
respondWithMiningError(requestContext, ErrDatabaseError("get historical stats").WithCause(err))
return
}
c.JSON(http.StatusOK, stats)
requestContext.JSON(http.StatusOK, stats)
}
// handleMinerHistoricalStats godoc
@ -1289,26 +1289,26 @@ func (service *Service) handleAllMinersHistoricalStats(c *gin.Context) {
// @Param miner_name path string true "Miner Name"
// @Success 200 {object} database.HashrateStats
// @Router /history/miners/{miner_name} [get]
func (service *Service) handleMinerHistoricalStats(c *gin.Context) {
minerName := c.Param("miner_name")
func (service *Service) handleMinerHistoricalStats(requestContext *gin.Context) {
minerName := requestContext.Param("miner_name")
manager, ok := service.Manager.(*Manager)
if !ok {
respondWithMiningError(c, ErrInternal("manager type not supported"))
respondWithMiningError(requestContext, ErrInternal("manager type not supported"))
return
}
stats, err := manager.GetMinerHistoricalStats(minerName)
if err != nil {
respondWithMiningError(c, ErrDatabaseError("get miner stats").WithCause(err))
respondWithMiningError(requestContext, ErrDatabaseError("get miner stats").WithCause(err))
return
}
if stats == nil {
respondWithMiningError(c, ErrMinerNotFound(minerName).WithDetails("no historical data found"))
respondWithMiningError(requestContext, ErrMinerNotFound(minerName).WithDetails("no historical data found"))
return
}
c.JSON(http.StatusOK, stats)
requestContext.JSON(http.StatusOK, stats)
}
// handleMinerHistoricalHashrate godoc
@ -1321,11 +1321,11 @@ func (service *Service) handleMinerHistoricalStats(c *gin.Context) {
// @Param until query string false "End time (RFC3339 format)"
// @Success 200 {array} HashratePoint
// @Router /history/miners/{miner_name}/hashrate [get]
func (service *Service) handleMinerHistoricalHashrate(c *gin.Context) {
minerName := c.Param("miner_name")
func (service *Service) handleMinerHistoricalHashrate(requestContext *gin.Context) {
minerName := requestContext.Param("miner_name")
manager, ok := service.Manager.(*Manager)
if !ok {
respondWithMiningError(c, ErrInternal("manager type not supported"))
respondWithMiningError(requestContext, ErrInternal("manager type not supported"))
return
}
@ -1333,12 +1333,12 @@ func (service *Service) handleMinerHistoricalHashrate(c *gin.Context) {
until := time.Now()
since := until.Add(-24 * time.Hour)
if sinceStr := c.Query("since"); sinceStr != "" {
if sinceStr := requestContext.Query("since"); sinceStr != "" {
if t, err := time.Parse(time.RFC3339, sinceStr); err == nil {
since = t
}
}
if untilStr := c.Query("until"); untilStr != "" {
if untilStr := requestContext.Query("until"); untilStr != "" {
if t, err := time.Parse(time.RFC3339, untilStr); err == nil {
until = t
}
@ -1346,11 +1346,11 @@ func (service *Service) handleMinerHistoricalHashrate(c *gin.Context) {
history, err := manager.GetMinerHistoricalHashrate(minerName, since, until)
if err != nil {
respondWithMiningError(c, ErrDatabaseError("get hashrate history").WithCause(err))
respondWithMiningError(requestContext, ErrDatabaseError("get hashrate history").WithCause(err))
return
}
c.JSON(http.StatusOK, history)
requestContext.JSON(http.StatusOK, history)
}
// handleWebSocketEvents godoc
@ -1360,19 +1360,19 @@ func (service *Service) handleMinerHistoricalHashrate(c *gin.Context) {
// @Tags websocket
// @Success 101 {string} string "Switching Protocols"
// @Router /ws/events [get]
func (service *Service) handleWebSocketEvents(c *gin.Context) {
conn, err := wsUpgrader.Upgrade(c.Writer, c.Request, nil)
func (service *Service) handleWebSocketEvents(requestContext *gin.Context) {
conn, err := wsUpgrader.Upgrade(requestContext.Writer, requestContext.Request, nil)
if err != nil {
logging.Error("failed to upgrade WebSocket connection", logging.Fields{"error": err})
return
}
logging.Info("new WebSocket connection", logging.Fields{"remote": c.Request.RemoteAddr})
logging.Info("new WebSocket connection", logging.Fields{"remote": requestContext.Request.RemoteAddr})
// Only record connection after successful registration to avoid metrics race
if service.EventHub.ServeWs(conn) {
RecordWSConnection(true)
} else {
logging.Warn("WebSocket connection rejected", logging.Fields{"remote": c.Request.RemoteAddr, "reason": "limit reached"})
logging.Warn("WebSocket connection rejected", logging.Fields{"remote": requestContext.Request.RemoteAddr, "reason": "limit reached"})
}
}
@ -1383,6 +1383,6 @@ func (service *Service) handleWebSocketEvents(c *gin.Context) {
// @Produce json
// @Success 200 {object} map[string]interface{}
// @Router /metrics [get]
func (service *Service) handleMetrics(c *gin.Context) {
c.JSON(http.StatusOK, GetMetricsSnapshot())
func (service *Service) handleMetrics(requestContext *gin.Context) {
requestContext.JSON(http.StatusOK, GetMetricsSnapshot())
}