Files
git.stella-ops.org/src/Platform/StellaOps.Platform.WebService/Endpoints/CryptoProviderAdminEndpoints.cs
master 59ba757eaa feat(crypto): extract crypto providers to overlay compose files + health probe API
- Extract smremote to docker-compose.crypto-provider.smremote.yml
- Rename cryptopro/crypto-sim compose files for consistent naming
- Add crypto provider health probe endpoint (CP-001)
- Add tenant crypto provider preferences API + migration (CP-002)
- Update docs and compliance env examples

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 13:21:50 +03:00

152 lines
5.9 KiB
C#

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using StellaOps.Auth.ServerIntegration.Tenancy;
using StellaOps.Platform.WebService.Constants;
using StellaOps.Platform.WebService.Contracts;
using StellaOps.Platform.WebService.Services;
using System;
namespace StellaOps.Platform.WebService.Endpoints;
/// <summary>
/// Admin endpoints for crypto provider health probing and tenant preference management.
/// CP-001: GET /api/v1/admin/crypto-providers/health
/// CP-002: GET/PUT/DELETE /api/v1/admin/crypto-providers/preferences
/// </summary>
public static class CryptoProviderAdminEndpoints
{
public static IEndpointRouteBuilder MapCryptoProviderAdminEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/v1/admin/crypto-providers")
.WithTags("CryptoProviders")
.RequireAuthorization(PlatformPolicies.HealthAdmin)
.RequireTenant();
// ---------------------------------------------------------------
// CP-001: Health probe
// ---------------------------------------------------------------
group.MapGet("/health", async Task<IResult>(
CryptoProviderHealthService healthService,
CancellationToken cancellationToken) =>
{
var result = await healthService.ProbeAllAsync(cancellationToken).ConfigureAwait(false);
return Results.Ok(result);
})
.WithName("GetCryptoProviderHealth")
.WithSummary("Probe crypto provider health")
.WithDescription(
"Probes each known crypto provider health endpoint and returns aggregated status. " +
"Unreachable providers return 'unreachable' status, not an error.");
// ---------------------------------------------------------------
// CP-002: Preferences CRUD
// ---------------------------------------------------------------
group.MapGet("/preferences", async Task<IResult>(
HttpContext context,
PlatformRequestContextResolver resolver,
ICryptoProviderPreferenceStore store,
TimeProvider timeProvider,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var rc, out var failure))
{
return failure!;
}
if (!Guid.TryParse(rc!.TenantId, out var tenantGuid))
{
return Results.BadRequest(new { error = "invalid_tenant_id" });
}
var preferences = await store.GetByTenantAsync(tenantGuid, cancellationToken).ConfigureAwait(false);
return Results.Ok(new CryptoProviderPreferencesResponse(
TenantId: rc.TenantId,
Preferences: preferences,
DataAsOf: timeProvider.GetUtcNow()));
})
.WithName("GetCryptoProviderPreferences")
.WithSummary("List tenant crypto provider preferences")
.WithDescription("Returns all crypto provider preferences for the current tenant, ordered by priority.");
group.MapPut("/preferences", async Task<IResult>(
HttpContext context,
PlatformRequestContextResolver resolver,
ICryptoProviderPreferenceStore store,
[FromBody] CryptoProviderPreferenceRequest request,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var rc, out var failure))
{
return failure!;
}
if (string.IsNullOrWhiteSpace(request.ProviderId))
{
return Results.BadRequest(new { error = "provider_id_required" });
}
if (!Guid.TryParse(rc!.TenantId, out var tenantGuid))
{
return Results.BadRequest(new { error = "invalid_tenant_id" });
}
var result = await store.UpsertAsync(tenantGuid, request, cancellationToken).ConfigureAwait(false);
return Results.Ok(result);
})
.WithName("UpsertCryptoProviderPreference")
.WithSummary("Create or update a crypto provider preference")
.WithDescription(
"Upserts a tenant crypto provider preference. The unique key is (tenantId, providerId, algorithmScope).");
group.MapDelete("/preferences/{id:guid}", async Task<IResult>(
HttpContext context,
PlatformRequestContextResolver resolver,
ICryptoProviderPreferenceStore store,
Guid id,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var rc, out var failure))
{
return failure!;
}
if (!Guid.TryParse(rc!.TenantId, out var tenantGuid))
{
return Results.BadRequest(new { error = "invalid_tenant_id" });
}
var deleted = await store.DeleteAsync(tenantGuid, id, cancellationToken).ConfigureAwait(false);
return deleted
? Results.NoContent()
: Results.NotFound(new { error = "preference_not_found", id });
})
.WithName("DeleteCryptoProviderPreference")
.WithSummary("Delete a crypto provider preference")
.WithDescription("Removes a single crypto provider preference by ID. Tenant-isolated.");
return app;
}
private static bool TryResolveContext(
HttpContext context,
PlatformRequestContextResolver resolver,
out PlatformRequestContext? requestContext,
out IResult? failure)
{
if (resolver.TryResolve(context, out requestContext, out var error))
{
failure = null;
return true;
}
failure = Results.BadRequest(new { error = error ?? "tenant_missing" });
return false;
}
}