Restructure solution layout by module
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user