Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
300 changes: 300 additions & 0 deletions BiatecTokensApi/Controllers/DeploymentStatusController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,22 @@ namespace BiatecTokensApi.Controllers
public class DeploymentStatusController : ControllerBase
{
private readonly IDeploymentStatusService _deploymentStatusService;
private readonly IDeploymentAuditService _auditService;
private readonly ILogger<DeploymentStatusController> _logger;

/// <summary>
/// Initializes a new instance of the <see cref="DeploymentStatusController"/> class.
/// </summary>
/// <param name="deploymentStatusService">The deployment status service</param>
/// <param name="auditService">The deployment audit service</param>
/// <param name="logger">The logger instance</param>
public DeploymentStatusController(
IDeploymentStatusService deploymentStatusService,
IDeploymentAuditService auditService,
ILogger<DeploymentStatusController> logger)
{
_deploymentStatusService = deploymentStatusService;
_auditService = auditService;
_logger = logger;
}

Expand Down Expand Up @@ -232,5 +236,301 @@ public async Task<IActionResult> GetDeploymentHistory([FromRoute] string deploym
});
}
}

/// <summary>
/// Cancels a pending deployment
/// </summary>
/// <param name="deploymentId">The deployment ID</param>
/// <param name="request">Cancellation request with reason</param>
/// <returns>Cancellation result</returns>
/// <remarks>
/// Cancels a deployment that is in Queued status. Once a deployment has been
/// submitted to the blockchain, it cannot be cancelled through this endpoint.
///
/// **Use Cases:**
/// - User changes mind before transaction submission
/// - User wants to modify parameters
/// - User realizes incorrect configuration
///
/// **Restrictions:**
/// - Only deployments in Queued status can be cancelled
/// - Submitted transactions cannot be cancelled (blockchain immutability)
/// </remarks>
[HttpPost("{deploymentId}/cancel")]
[ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> CancelDeployment(
[FromRoute] string deploymentId,
[FromBody] CancelDeploymentRequest request)
{
try
{
if (string.IsNullOrWhiteSpace(deploymentId))
{
return BadRequest(new
{
success = false,
errorMessage = "DeploymentId is required"
});
}

if (string.IsNullOrWhiteSpace(request?.Reason))
{
return BadRequest(new
{
success = false,
errorMessage = "Cancellation reason is required"
});
}

var result = await _deploymentStatusService.CancelDeploymentAsync(deploymentId, request.Reason);

if (!result)
{
return BadRequest(new
{
success = false,
errorMessage = "Deployment cannot be cancelled. It may not exist or may have already been submitted."
});
}

_logger.LogInformation("Deployment cancelled: DeploymentId={DeploymentId}, Reason={Reason}",
LoggingHelper.SanitizeLogInput(deploymentId), LoggingHelper.SanitizeLogInput(request.Reason));

return Ok(new
{
success = true,
message = "Deployment cancelled successfully"
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Error cancelling deployment: DeploymentId={DeploymentId}", LoggingHelper.SanitizeLogInput(deploymentId));
return StatusCode(StatusCodes.Status500InternalServerError, new
{
success = false,
errorMessage = "An error occurred while cancelling the deployment"
});
}
}

/// <summary>
/// Exports audit trail for a specific deployment
/// </summary>
/// <param name="deploymentId">The deployment ID</param>
/// <param name="format">Export format (json or csv)</param>
/// <returns>Audit trail in requested format</returns>
/// <remarks>
/// Exports complete audit trail for a deployment including:
/// - All status transitions with timestamps
/// - Compliance checks performed
/// - Error details if applicable
/// - Transaction information
/// - Duration metrics
///
/// **Use Cases:**
/// - Regulatory compliance reporting
/// - Incident investigation
/// - Performance analysis
/// - Customer support documentation
///
/// **Formats:**
/// - JSON: Full structured data with nested objects
/// - CSV: Flattened data suitable for spreadsheet analysis
/// </remarks>
[HttpGet("{deploymentId}/audit-trail")]
[ProducesResponseType(typeof(string), StatusCodes.Status200OK, "application/json")]
[ProducesResponseType(typeof(string), StatusCodes.Status200OK, "text/csv")]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> ExportAuditTrail(
[FromRoute] string deploymentId,
[FromQuery] string format = "json")
{
try
{
if (string.IsNullOrWhiteSpace(deploymentId))
{
return BadRequest(new
{
success = false,
errorMessage = "DeploymentId is required"
});
}

// Validate format
if (format.ToLower() != "json" && format.ToLower() != "csv")
{
return BadRequest(new
{
success = false,
errorMessage = "Format must be 'json' or 'csv'"
});
}

string data;
string contentType;
string fileName;

if (format.ToLower() == "json")
{
data = await _auditService.ExportAuditTrailAsJsonAsync(deploymentId);
contentType = "application/json";
fileName = $"audit-trail-{deploymentId}.json";
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fileName constructed using string interpolation directly includes the unsanitized deploymentId parameter. While the deploymentId should be a GUID, if a malicious user provides path traversal characters or special characters, it could potentially cause issues with file download behavior in some browsers. Consider sanitizing the deploymentId or using a safe filename pattern that doesn't directly expose user input.

Copilot uses AI. Check for mistakes.
}
else
{
data = await _auditService.ExportAuditTrailAsCsvAsync(deploymentId);
contentType = "text/csv";
fileName = $"audit-trail-{deploymentId}.csv";
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fileName constructed using string interpolation directly includes the unsanitized deploymentId parameter. This is the same issue as line 380 - consider sanitizing the deploymentId or using a safe filename pattern.

Copilot uses AI. Check for mistakes.
}

_logger.LogInformation("Exported audit trail: DeploymentId={DeploymentId}, Format={Format}, Size={Size}",
LoggingHelper.SanitizeLogInput(deploymentId), format, data.Length);

return File(System.Text.Encoding.UTF8.GetBytes(data), contentType, fileName);
}
catch (InvalidOperationException ex)
{
_logger.LogWarning("Deployment not found for audit trail: DeploymentId={DeploymentId}, Error={Error}",
LoggingHelper.SanitizeLogInput(deploymentId), ex.Message);
return NotFound(new
{
success = false,
errorMessage = ex.Message
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Error exporting audit trail: DeploymentId={DeploymentId}",
LoggingHelper.SanitizeLogInput(deploymentId));
return StatusCode(StatusCodes.Status500InternalServerError, new
{
success = false,
errorMessage = "An error occurred while exporting the audit trail"
});
}
}

/// <summary>
/// Exports audit trails for multiple deployments
/// </summary>
/// <param name="request">Export request with filters</param>
/// <returns>Audit trails in requested format</returns>
/// <remarks>
/// Exports audit trails for multiple deployments with filtering and pagination.
/// Supports idempotency through the X-Idempotency-Key header for large exports.
///
/// **Use Cases:**
/// - Bulk compliance reporting
/// - Historical analysis
/// - Data migration
/// - Backup and archival
///
/// **Idempotency:**
/// - Include X-Idempotency-Key header for large exports
/// - Results are cached for 1 hour
/// - Repeated requests with same key return cached results
/// - Key must be unique per unique request parameters
///
/// **Pagination:**
/// - Default: 100 records per page
/// - Maximum: 1000 records per page
/// - Use multiple requests for larger datasets
/// </remarks>
[HttpPost("audit-trail/export")]
[ProducesResponseType(typeof(AuditExportResult), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> ExportAuditTrails([FromBody] AuditExportRequest request)
{
try
{
// Get idempotency key from header
var idempotencyKey = Request.Headers["X-Idempotency-Key"].FirstOrDefault();

var result = await _auditService.ExportAuditTrailsAsync(request, idempotencyKey);

if (!result.Success)
{
return BadRequest(result);
}

_logger.LogInformation("Exported audit trails: Count={Count}, Format={Format}, Cached={Cached}",
result.RecordCount, request.Format, result.IsCached);

return Ok(result);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error exporting audit trails");
return StatusCode(StatusCodes.Status500InternalServerError, new AuditExportResult
{
Success = false,
ErrorMessage = "An error occurred while exporting audit trails"
});
}
}

/// <summary>
/// Gets deployment metrics for monitoring and analytics
/// </summary>
/// <param name="request">Metrics request with filters</param>
/// <returns>Deployment metrics</returns>
/// <remarks>
/// Returns comprehensive deployment metrics including:
/// - Success/failure rates
/// - Duration statistics (average, median, P95)
/// - Failure breakdown by category
/// - Deployment counts by network and token type
/// - Average duration by status transition
/// - Retry statistics
///
/// **Use Cases:**
/// - Monitoring dashboard creation
/// - SLA tracking and reporting
/// - Performance optimization
/// - Capacity planning
/// - Customer success metrics
///
/// **Time Period:**
/// - Default: Last 24 hours
/// - Custom: Specify fromDate and toDate
/// - Maximum recommended period: 30 days (for performance)
///
/// **Filtering:**
/// - By network (e.g., "voimain-v1.0", "base-mainnet")
/// - By token type (e.g., "ERC20_Mintable", "ARC200_Mintable")
/// - By deployer address
/// </remarks>
[HttpGet("metrics")]
[ProducesResponseType(typeof(DeploymentMetricsResponse), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> GetDeploymentMetrics([FromQuery] GetDeploymentMetricsRequest request)
{
try
{
var metrics = await _deploymentStatusService.GetDeploymentMetricsAsync(request);

_logger.LogInformation("Calculated deployment metrics: Period={FromDate} to {ToDate}, Total={Total}, Success={Success}, Failed={Failed}",
metrics.PeriodStart, metrics.PeriodEnd, metrics.TotalDeployments,
metrics.SuccessfulDeployments, metrics.FailedDeployments);

return Ok(new DeploymentMetricsResponse
{
Success = true,
Metrics = metrics
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Error calculating deployment metrics");
return StatusCode(StatusCodes.Status500InternalServerError, new DeploymentMetricsResponse
{
Success = false,
ErrorMessage = "An error occurred while calculating deployment metrics"
});
}
}
}
}
Loading
Loading