mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 16:33:12 +00:00
114 lines
4.4 KiB
C#
114 lines
4.4 KiB
C#
using System.Buffers;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.Net.Http.Headers;
|
|
using SeaHaven.Services.Interfaces;
|
|
|
|
namespace Api.SeaHavenIndustries.Controllers
|
|
{
|
|
[ApiController]
|
|
[AllowAnonymous]
|
|
[Route("api/webhooks/work-orders")]
|
|
public sealed class WorkOrderWebhookController : ControllerBase
|
|
{
|
|
private readonly IWorkOrderWebhookService _service;
|
|
|
|
public WorkOrderWebhookController(IWorkOrderWebhookService service)
|
|
{
|
|
_service = service;
|
|
}
|
|
|
|
[HttpPost]
|
|
[Consumes("application/json")]
|
|
public async Task<IActionResult> Receive(CancellationToken cancellationToken)
|
|
{
|
|
if (!HasSupportedContentType(Request.ContentType))
|
|
return StatusCode(StatusCodes.Status415UnsupportedMediaType);
|
|
|
|
if (Request.Headers.ContentEncoding.Count > 0
|
|
&& !string.Equals(
|
|
Request.Headers.ContentEncoding.ToString(),
|
|
"identity",
|
|
StringComparison.OrdinalIgnoreCase))
|
|
return StatusCode(StatusCodes.Status415UnsupportedMediaType);
|
|
|
|
var maxBytes = _service.MaximumBodyBytes;
|
|
if (Request.ContentLength > maxBytes)
|
|
return StatusCode(StatusCodes.Status413PayloadTooLarge);
|
|
|
|
var body = await ReadBoundedBodyAsync(Request.Body, maxBytes, cancellationToken);
|
|
if (body == null)
|
|
return StatusCode(StatusCodes.Status413PayloadTooLarge);
|
|
|
|
var result = await _service.ProcessAsync(
|
|
new WorkOrderWebhookRequest(
|
|
SingleHeader("X-SH-Timestamp"),
|
|
SingleHeader("X-SH-Key-Id"),
|
|
SingleHeader("X-SH-Signature"),
|
|
body),
|
|
cancellationToken);
|
|
|
|
return result.Status switch
|
|
{
|
|
WorkOrderWebhookStatus.Applied => Ok(new
|
|
{
|
|
status = "accepted",
|
|
state_mutation_skipped = result.StateMutationSkipped
|
|
}),
|
|
WorkOrderWebhookStatus.Duplicate => Ok(new { status = "duplicate" }),
|
|
WorkOrderWebhookStatus.Unauthorized => Unauthorized(new { error = "unauthorized" }),
|
|
WorkOrderWebhookStatus.Disabled => StatusCode(
|
|
StatusCodes.Status503ServiceUnavailable,
|
|
new { error = "webhook unavailable" }),
|
|
WorkOrderWebhookStatus.InvalidEnvelope => BadRequest(new { error = "invalid envelope" }),
|
|
WorkOrderWebhookStatus.HashConflict => Conflict(new { error = "delivery conflict" }),
|
|
WorkOrderWebhookStatus.Unavailable => StatusCode(
|
|
StatusCodes.Status503ServiceUnavailable,
|
|
new { error = "webhook unavailable" }),
|
|
_ => StatusCode(StatusCodes.Status503ServiceUnavailable)
|
|
};
|
|
}
|
|
|
|
private string? SingleHeader(string name)
|
|
{
|
|
var values = Request.Headers[name];
|
|
return values.Count == 1 ? values[0] : null;
|
|
}
|
|
|
|
private static bool HasSupportedContentType(string? contentType)
|
|
{
|
|
return MediaTypeHeaderValue.TryParse(contentType, out var parsed)
|
|
&& string.Equals(
|
|
parsed.MediaType.Value,
|
|
"application/json",
|
|
StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
private static async Task<byte[]?> ReadBoundedBodyAsync(
|
|
Stream source,
|
|
int maxBytes,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var rented = ArrayPool<byte>.Shared.Rent(81920);
|
|
try
|
|
{
|
|
using var destination = new MemoryStream(Math.Min(maxBytes, 81920));
|
|
while (true)
|
|
{
|
|
var read = await source.ReadAsync(rented.AsMemory(0, rented.Length), cancellationToken);
|
|
if (read == 0)
|
|
return destination.ToArray();
|
|
|
|
if (destination.Length + read > maxBytes)
|
|
return null;
|
|
|
|
await destination.WriteAsync(rented.AsMemory(0, read), cancellationToken);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
ArrayPool<byte>.Shared.Return(rented, clearArray: true);
|
|
}
|
|
}
|
|
}
|
|
}
|