save progress
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Auth.Abstractions;
|
||||
using StellaOps.Scanner.Core.Configuration;
|
||||
using StellaOps.Scanner.WebService.Constants;
|
||||
using StellaOps.Scanner.WebService.Infrastructure;
|
||||
using StellaOps.Scanner.WebService.Security;
|
||||
using StellaOps.Scanner.WebService.Services;
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Endpoints;
|
||||
|
||||
internal static class OfflineKitEndpoints
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
public static void MapOfflineKitEndpoints(this IEndpointRouteBuilder endpoints)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(endpoints);
|
||||
|
||||
var group = endpoints
|
||||
.MapGroup("/api/offline-kit")
|
||||
.WithTags("Offline Kit");
|
||||
|
||||
group.MapPost("/import", HandleImportAsync)
|
||||
.WithName("scanner.offline-kit.import")
|
||||
.RequireAuthorization(ScannerPolicies.OfflineKitImport)
|
||||
.Produces<OfflineKitImportResponseTransport>(StatusCodes.Status202Accepted)
|
||||
.Produces<ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||
.Produces<ProblemDetails>(StatusCodes.Status404NotFound)
|
||||
.Produces<ProblemDetails>(StatusCodes.Status422UnprocessableEntity);
|
||||
|
||||
group.MapGet("/status", HandleStatusAsync)
|
||||
.WithName("scanner.offline-kit.status")
|
||||
.RequireAuthorization(ScannerPolicies.OfflineKitStatusRead)
|
||||
.Produces<OfflineKitStatusTransport>(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status204NoContent)
|
||||
.Produces<ProblemDetails>(StatusCodes.Status404NotFound);
|
||||
}
|
||||
|
||||
private static async Task<IResult> HandleImportAsync(
|
||||
HttpContext context,
|
||||
HttpRequest request,
|
||||
IOptionsMonitor<OfflineKitOptions> offlineKitOptions,
|
||||
OfflineKitImportService importService,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
ArgumentNullException.ThrowIfNull(offlineKitOptions);
|
||||
ArgumentNullException.ThrowIfNull(importService);
|
||||
|
||||
if (!offlineKitOptions.CurrentValue.Enabled)
|
||||
{
|
||||
return ProblemResultFactory.Create(
|
||||
context,
|
||||
ProblemTypes.NotFound,
|
||||
"Offline kit import is not enabled",
|
||||
StatusCodes.Status404NotFound);
|
||||
}
|
||||
|
||||
if (!request.HasFormContentType)
|
||||
{
|
||||
return ProblemResultFactory.Create(
|
||||
context,
|
||||
ProblemTypes.Validation,
|
||||
"Invalid offline kit import request",
|
||||
StatusCodes.Status400BadRequest,
|
||||
detail: "Request must be multipart/form-data.");
|
||||
}
|
||||
|
||||
var form = await request.ReadFormAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var metadataJson = form["metadata"].FirstOrDefault();
|
||||
if (string.IsNullOrWhiteSpace(metadataJson))
|
||||
{
|
||||
return ProblemResultFactory.Create(
|
||||
context,
|
||||
ProblemTypes.Validation,
|
||||
"Invalid offline kit import request",
|
||||
StatusCodes.Status400BadRequest,
|
||||
detail: "Missing 'metadata' form field.");
|
||||
}
|
||||
|
||||
OfflineKitImportMetadata? metadata;
|
||||
try
|
||||
{
|
||||
metadata = JsonSerializer.Deserialize<OfflineKitImportMetadata>(metadataJson, JsonOptions);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
return ProblemResultFactory.Create(
|
||||
context,
|
||||
ProblemTypes.Validation,
|
||||
"Invalid offline kit import request",
|
||||
StatusCodes.Status400BadRequest,
|
||||
detail: $"Failed to parse metadata JSON: {ex.Message}");
|
||||
}
|
||||
|
||||
if (metadata is null)
|
||||
{
|
||||
return ProblemResultFactory.Create(
|
||||
context,
|
||||
ProblemTypes.Validation,
|
||||
"Invalid offline kit import request",
|
||||
StatusCodes.Status400BadRequest,
|
||||
detail: "Metadata payload is empty.");
|
||||
}
|
||||
|
||||
var bundle = form.Files.GetFile("bundle");
|
||||
if (bundle is null)
|
||||
{
|
||||
return ProblemResultFactory.Create(
|
||||
context,
|
||||
ProblemTypes.Validation,
|
||||
"Invalid offline kit import request",
|
||||
StatusCodes.Status400BadRequest,
|
||||
detail: "Missing 'bundle' file upload.");
|
||||
}
|
||||
|
||||
var manifest = form.Files.GetFile("manifest");
|
||||
var bundleSignature = form.Files.GetFile("bundleSignature");
|
||||
var manifestSignature = form.Files.GetFile("manifestSignature");
|
||||
|
||||
var tenantId = ResolveTenant(context);
|
||||
var actor = ResolveActor(context);
|
||||
|
||||
try
|
||||
{
|
||||
var response = await importService.ImportAsync(
|
||||
new OfflineKitImportRequest(
|
||||
tenantId,
|
||||
actor,
|
||||
metadata,
|
||||
bundle,
|
||||
manifest,
|
||||
bundleSignature,
|
||||
manifestSignature),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.Accepted("/api/offline-kit/status", response);
|
||||
}
|
||||
catch (OfflineKitImportException ex)
|
||||
{
|
||||
return ProblemResultFactory.Create(
|
||||
context,
|
||||
ProblemTypes.Validation,
|
||||
"Offline kit import failed",
|
||||
ex.StatusCode,
|
||||
detail: ex.Message,
|
||||
extensions: new Dictionary<string, object?>
|
||||
{
|
||||
["reason_code"] = ex.ReasonCode,
|
||||
["notes"] = ex.Notes
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<IResult> HandleStatusAsync(
|
||||
HttpContext context,
|
||||
IOptionsMonitor<OfflineKitOptions> offlineKitOptions,
|
||||
OfflineKitStateStore stateStore,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
ArgumentNullException.ThrowIfNull(offlineKitOptions);
|
||||
ArgumentNullException.ThrowIfNull(stateStore);
|
||||
|
||||
if (!offlineKitOptions.CurrentValue.Enabled)
|
||||
{
|
||||
return ProblemResultFactory.Create(
|
||||
context,
|
||||
ProblemTypes.NotFound,
|
||||
"Offline kit status is not enabled",
|
||||
StatusCodes.Status404NotFound);
|
||||
}
|
||||
|
||||
var tenantId = ResolveTenant(context);
|
||||
var status = await stateStore.LoadStatusAsync(tenantId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return status is null
|
||||
? Results.NoContent()
|
||||
: Results.Ok(status);
|
||||
}
|
||||
|
||||
private static string ResolveTenant(HttpContext context)
|
||||
{
|
||||
var tenant = context.User?.FindFirstValue(StellaOpsClaimTypes.Tenant);
|
||||
if (!string.IsNullOrWhiteSpace(tenant))
|
||||
{
|
||||
return tenant.Trim();
|
||||
}
|
||||
|
||||
if (context.Request.Headers.TryGetValue("X-Stella-Tenant", out var headerTenant))
|
||||
{
|
||||
var headerValue = headerTenant.ToString();
|
||||
if (!string.IsNullOrWhiteSpace(headerValue))
|
||||
{
|
||||
return headerValue.Trim();
|
||||
}
|
||||
}
|
||||
|
||||
return "default";
|
||||
}
|
||||
|
||||
private static string ResolveActor(HttpContext context)
|
||||
{
|
||||
var subject = context.User?.FindFirstValue(StellaOpsClaimTypes.Subject);
|
||||
if (!string.IsNullOrWhiteSpace(subject))
|
||||
{
|
||||
return subject.Trim();
|
||||
}
|
||||
|
||||
var clientId = context.User?.FindFirstValue(StellaOpsClaimTypes.ClientId);
|
||||
if (!string.IsNullOrWhiteSpace(clientId))
|
||||
{
|
||||
return clientId.Trim();
|
||||
}
|
||||
|
||||
return "anonymous";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user