using Microsoft.AspNetCore.Mvc; using StellaOps.AirGap.Time.Models; using StellaOps.AirGap.Time.Services; namespace StellaOps.AirGap.Time.Controllers; [ApiController] [Route("api/v1/time")] public class TimeStatusController : ControllerBase { private readonly TimeStatusService _statusService; private readonly TimeAnchorLoader _loader; private readonly TrustRootProvider _trustRoots; private readonly TimeProvider _timeProvider; private readonly ILogger _logger; public TimeStatusController( TimeStatusService statusService, TimeAnchorLoader loader, TrustRootProvider trustRoots, TimeProvider timeProvider, ILogger logger) { _statusService = statusService; _loader = loader; _trustRoots = trustRoots; _timeProvider = timeProvider; _logger = logger; } [HttpGet("status")] public async Task> GetStatusAsync([FromQuery] string tenantId) { if (string.IsNullOrWhiteSpace(tenantId)) { return BadRequest("tenantId-required"); } var status = await _statusService.GetStatusAsync(tenantId, _timeProvider.GetUtcNow(), HttpContext.RequestAborted).ConfigureAwait(false); return Ok(TimeStatusDto.FromStatus(status)); } [HttpPost("anchor")] public async Task> SetAnchorAsync([FromBody] SetAnchorRequest request) { if (!ModelState.IsValid) { return ValidationProblem(ModelState); } var trustRoots = _trustRoots.GetAll(); if (!string.IsNullOrWhiteSpace(request.TrustRootPublicKeyBase64)) { try { var publicKey = Convert.FromBase64String(request.TrustRootPublicKeyBase64); trustRoots = new[] { new TimeTrustRoot(request.TrustRootKeyId, publicKey, request.TrustRootAlgorithm) }; } catch (FormatException) { return BadRequest("trust-root-public-key-invalid-base64"); } } var result = _loader.TryLoadHex( request.HexToken, request.Format, trustRoots, out var anchor); if (!result.IsValid) { _logger.LogWarning("Failed to ingest time anchor for tenant {Tenant}: {Reason}", request.TenantId, result.Reason); return BadRequest(result.Reason); } var budget = new StalenessBudget( request.WarningSeconds ?? StalenessBudget.Default.WarningSeconds, request.BreachSeconds ?? StalenessBudget.Default.BreachSeconds); await _statusService.SetAnchorAsync(request.TenantId, anchor, budget, HttpContext.RequestAborted).ConfigureAwait(false); _logger.LogInformation("Time anchor set for tenant {Tenant} format={Format} digest={Digest} warning={Warning}s breach={Breach}s", request.TenantId, anchor.Format, anchor.TokenDigest, budget.WarningSeconds, budget.BreachSeconds); var status = await _statusService.GetStatusAsync(request.TenantId, _timeProvider.GetUtcNow(), HttpContext.RequestAborted).ConfigureAwait(false); return Ok(TimeStatusDto.FromStatus(status)); } }