save development progress

This commit is contained in:
StellaOps Bot
2025-12-25 23:09:58 +02:00
parent d71853ad7e
commit aa70af062e
351 changed files with 37683 additions and 150156 deletions

View File

@@ -0,0 +1,175 @@
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using StellaOps.Findings.Ledger.WebService.Contracts;
using StellaOps.Findings.Ledger.WebService.Services;
namespace StellaOps.Findings.Ledger.WebService.Endpoints;
/// <summary>
/// Webhook management endpoints.
/// Sprint: SPRINT_8200_0012_0004 - Wave 6
/// </summary>
public static class WebhookEndpoints
{
// Authorization policy name (must match Program.cs)
private const string ScoringAdminPolicy = "scoring.admin";
public static void MapWebhookEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/v1/scoring/webhooks")
.WithTags("Webhooks");
// POST /api/v1/scoring/webhooks - Register webhook
// Rate limit: 10/min (via API Gateway)
group.MapPost("/", RegisterWebhook)
.WithName("RegisterScoringWebhook")
.WithDescription("Register a webhook for score change notifications")
.Produces<WebhookResponse>(StatusCodes.Status201Created)
.ProducesValidationProblem()
.RequireAuthorization(ScoringAdminPolicy);
// GET /api/v1/scoring/webhooks - List webhooks
// Rate limit: 10/min (via API Gateway)
group.MapGet("/", ListWebhooks)
.WithName("ListScoringWebhooks")
.WithDescription("List all registered webhooks")
.Produces<WebhookListResponse>(StatusCodes.Status200OK)
.RequireAuthorization(ScoringAdminPolicy);
// GET /api/v1/scoring/webhooks/{id} - Get webhook
// Rate limit: 10/min (via API Gateway)
group.MapGet("/{id:guid}", GetWebhook)
.WithName("GetScoringWebhook")
.WithDescription("Get a specific webhook by ID")
.Produces<WebhookResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.RequireAuthorization(ScoringAdminPolicy);
// PUT /api/v1/scoring/webhooks/{id} - Update webhook
// Rate limit: 10/min (via API Gateway)
group.MapPut("/{id:guid}", UpdateWebhook)
.WithName("UpdateScoringWebhook")
.WithDescription("Update a webhook configuration")
.Produces<WebhookResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.ProducesValidationProblem()
.RequireAuthorization(ScoringAdminPolicy);
// DELETE /api/v1/scoring/webhooks/{id} - Delete webhook
// Rate limit: 10/min (via API Gateway)
group.MapDelete("/{id:guid}", DeleteWebhook)
.WithName("DeleteScoringWebhook")
.WithDescription("Delete a webhook")
.Produces(StatusCodes.Status204NoContent)
.Produces(StatusCodes.Status404NotFound)
.RequireAuthorization(ScoringAdminPolicy);
}
private static Results<Created<WebhookResponse>, ValidationProblem> RegisterWebhook(
[FromBody] RegisterWebhookRequest request,
[FromServices] IWebhookStore store)
{
// Validate URL
if (!Uri.TryCreate(request.Url, UriKind.Absolute, out var uri) ||
(uri.Scheme != "http" && uri.Scheme != "https"))
{
return TypedResults.ValidationProblem(new Dictionary<string, string[]>
{
["url"] = ["Invalid webhook URL. Must be an absolute HTTP or HTTPS URL."]
});
}
var registration = store.Register(request);
var response = MapToResponse(registration);
return TypedResults.Created($"/api/v1/scoring/webhooks/{registration.Id}", response);
}
private static Ok<WebhookListResponse> ListWebhooks(
[FromServices] IWebhookStore store)
{
var webhooks = store.List();
var response = new WebhookListResponse
{
Webhooks = webhooks.Select(MapToResponse).ToList(),
TotalCount = webhooks.Count
};
return TypedResults.Ok(response);
}
private static Results<Ok<WebhookResponse>, NotFound> GetWebhook(
Guid id,
[FromServices] IWebhookStore store)
{
var webhook = store.Get(id);
if (webhook is null || !webhook.IsActive)
{
return TypedResults.NotFound();
}
return TypedResults.Ok(MapToResponse(webhook));
}
private static Results<Ok<WebhookResponse>, NotFound, ValidationProblem> UpdateWebhook(
Guid id,
[FromBody] RegisterWebhookRequest request,
[FromServices] IWebhookStore store)
{
// Validate URL
if (!Uri.TryCreate(request.Url, UriKind.Absolute, out var uri) ||
(uri.Scheme != "http" && uri.Scheme != "https"))
{
return TypedResults.ValidationProblem(new Dictionary<string, string[]>
{
["url"] = ["Invalid webhook URL. Must be an absolute HTTP or HTTPS URL."]
});
}
if (!store.Update(id, request))
{
return TypedResults.NotFound();
}
var updated = store.Get(id);
return TypedResults.Ok(MapToResponse(updated!));
}
private static Results<NoContent, NotFound> DeleteWebhook(
Guid id,
[FromServices] IWebhookStore store)
{
if (!store.Delete(id))
{
return TypedResults.NotFound();
}
return TypedResults.NoContent();
}
private static WebhookResponse MapToResponse(WebhookRegistration registration)
{
return new WebhookResponse
{
Id = registration.Id,
Url = registration.Url,
HasSecret = !string.IsNullOrEmpty(registration.Secret),
FindingPatterns = registration.FindingPatterns,
MinScoreChange = registration.MinScoreChange,
TriggerOnBucketChange = registration.TriggerOnBucketChange,
CreatedAt = registration.CreatedAt
};
}
}
/// <summary>
/// Response for listing webhooks.
/// </summary>
public sealed record WebhookListResponse
{
/// <summary>List of webhooks.</summary>
public required IReadOnlyList<WebhookResponse> Webhooks { get; init; }
/// <summary>Total count of webhooks.</summary>
public required int TotalCount { get; init; }
}