Restructure solution layout by module

This commit is contained in:
master
2025-10-28 15:10:40 +02:00
parent 95daa159c4
commit d870da18ce
4103 changed files with 192899 additions and 187024 deletions

View File

@@ -0,0 +1,173 @@
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Scheduler.WebService.Options;
namespace StellaOps.Scheduler.WebService.EventWebhooks;
public static class EventWebhookEndpointExtensions
{
public static void MapSchedulerEventWebhookEndpoints(this IEndpointRouteBuilder builder)
{
var group = builder.MapGroup("/events");
group.MapPost("/feedser-export", HandleFeedserExportAsync);
group.MapPost("/vexer-export", HandleVexerExportAsync);
}
private static async Task<IResult> HandleFeedserExportAsync(
HttpContext httpContext,
[FromServices] IOptionsMonitor<SchedulerEventsOptions> options,
[FromServices] IWebhookRequestAuthenticator authenticator,
[FromServices] IWebhookRateLimiter rateLimiter,
[FromServices] IInboundExportEventSink sink,
CancellationToken cancellationToken)
{
var webhookOptions = options.CurrentValue.Webhooks.Feedser;
if (!webhookOptions.Enabled)
{
return Results.StatusCode(StatusCodes.Status503ServiceUnavailable);
}
var readResult = await ReadPayloadAsync<FeedserExportEventRequest>(httpContext, cancellationToken).ConfigureAwait(false);
if (!readResult.Succeeded)
{
return readResult.ErrorResult!;
}
if (!rateLimiter.TryAcquire("feedser", webhookOptions.RateLimitRequests, webhookOptions.GetRateLimitWindow(), out var retryAfter))
{
var response = Results.StatusCode(StatusCodes.Status429TooManyRequests);
if (retryAfter > TimeSpan.Zero)
{
httpContext.Response.Headers.RetryAfter = ((int)Math.Ceiling(retryAfter.TotalSeconds)).ToString();
}
return response;
}
var authResult = await authenticator.AuthenticateAsync(httpContext, readResult.RawBody, webhookOptions, cancellationToken).ConfigureAwait(false);
if (!authResult.Succeeded)
{
return authResult.ToResult();
}
try
{
await sink.HandleFeedserAsync(readResult.Payload!, cancellationToken).ConfigureAwait(false);
return Results.Accepted(value: new { status = "accepted" });
}
catch (ValidationException ex)
{
return Results.BadRequest(new { error = ex.Message });
}
}
private static async Task<IResult> HandleVexerExportAsync(
HttpContext httpContext,
[FromServices] IOptionsMonitor<SchedulerEventsOptions> options,
[FromServices] IWebhookRequestAuthenticator authenticator,
[FromServices] IWebhookRateLimiter rateLimiter,
[FromServices] IInboundExportEventSink sink,
CancellationToken cancellationToken)
{
var webhookOptions = options.CurrentValue.Webhooks.Vexer;
if (!webhookOptions.Enabled)
{
return Results.StatusCode(StatusCodes.Status503ServiceUnavailable);
}
var readResult = await ReadPayloadAsync<VexerExportEventRequest>(httpContext, cancellationToken).ConfigureAwait(false);
if (!readResult.Succeeded)
{
return readResult.ErrorResult!;
}
if (!rateLimiter.TryAcquire("vexer", webhookOptions.RateLimitRequests, webhookOptions.GetRateLimitWindow(), out var retryAfter))
{
var response = Results.StatusCode(StatusCodes.Status429TooManyRequests);
if (retryAfter > TimeSpan.Zero)
{
httpContext.Response.Headers.RetryAfter = ((int)Math.Ceiling(retryAfter.TotalSeconds)).ToString();
}
return response;
}
var authResult = await authenticator.AuthenticateAsync(httpContext, readResult.RawBody, webhookOptions, cancellationToken).ConfigureAwait(false);
if (!authResult.Succeeded)
{
return authResult.ToResult();
}
try
{
await sink.HandleVexerAsync(readResult.Payload!, cancellationToken).ConfigureAwait(false);
return Results.Accepted(value: new { status = "accepted" });
}
catch (ValidationException ex)
{
return Results.BadRequest(new { error = ex.Message });
}
}
private static async Task<RequestPayload<T>> ReadPayloadAsync<T>(HttpContext context, CancellationToken cancellationToken)
{
context.Request.EnableBuffering();
await using var buffer = new MemoryStream();
await context.Request.Body.CopyToAsync(buffer, cancellationToken).ConfigureAwait(false);
var bodyBytes = buffer.ToArray();
context.Request.Body.Position = 0;
try
{
var payload = JsonSerializer.Deserialize<T>(bodyBytes, new JsonSerializerOptions(JsonSerializerDefaults.Web));
if (payload is null)
{
return RequestPayload<T>.Failed(Results.BadRequest(new { error = "Request payload cannot be empty." }));
}
return RequestPayload<T>.Success(payload, bodyBytes);
}
catch (JsonException ex)
{
return RequestPayload<T>.Failed(Results.BadRequest(new { error = ex.Message }));
}
catch (ValidationException ex)
{
return RequestPayload<T>.Failed(Results.BadRequest(new { error = ex.Message }));
}
}
private readonly struct RequestPayload<T>
{
private RequestPayload(T? payload, byte[] rawBody, IResult? error, bool succeeded)
{
Payload = payload;
RawBody = rawBody;
ErrorResult = error;
Succeeded = succeeded;
}
public T? Payload { get; }
public byte[] RawBody { get; }
public IResult? ErrorResult { get; }
public bool Succeeded { get; }
public static RequestPayload<T> Success(T payload, byte[] rawBody)
=> new(payload, rawBody, null, true);
public static RequestPayload<T> Failed(IResult error)
=> new(default, Array.Empty<byte>(), error, false);
}
}