feat: Implement provisioning pipelines for subscription management

- Add ReactivatePipeline to handle subscription reactivation, including scaling Docker services, health verification, status updates, audit logging, and broadcasting status changes.
- Introduce RotateCredentialsPipeline for OAuth2 credential rotation, managing the deletion of old apps, creation of new ones, credential storage, access verification, and audit logging.
- Create StepRunner to manage job step execution, including lifecycle management and progress broadcasting via SignalR.
- Implement SuspendPipeline for subscription suspension, scaling down services, updating statuses, logging audits, and broadcasting changes.
- Add UpdateScreenLimitPipeline to update Xibo CMS screen limits and record snapshots.
- Introduce XiboFeatureManifests for hardcoded feature ACLs per role.
- Add docker-compose.dev.yml for local development with PostgreSQL setup.
This commit is contained in:
Matt Batchelder
2026-03-18 10:27:26 -04:00
parent c2e03de8bb
commit c6d46098dd
77 changed files with 9412 additions and 29 deletions

View File

@@ -0,0 +1,60 @@
using OTSSignsOrchestrator.Server.Clients;
using OTSSignsOrchestrator.Server.Data.Entities;
namespace OTSSignsOrchestrator.Server.Health.Checks;
/// <summary>
/// Verifies the per-instance SAML provider in Authentik is active by checking
/// the provider exists using the stored <see cref="Instance.AuthentikProviderId"/>.
/// </summary>
public sealed class AuthentikSamlProviderHealthCheck : IHealthCheck
{
private readonly IAuthentikClient _authentikClient;
private readonly ILogger<AuthentikSamlProviderHealthCheck> _logger;
public string CheckName => "AuthentikSamlProvider";
public bool AutoRemediate => false;
public AuthentikSamlProviderHealthCheck(
IAuthentikClient authentikClient,
ILogger<AuthentikSamlProviderHealthCheck> logger)
{
_authentikClient = authentikClient;
_logger = logger;
}
public async Task<HealthCheckResult> RunAsync(Instance instance, CancellationToken ct)
{
if (string.IsNullOrEmpty(instance.AuthentikProviderId))
{
return new HealthCheckResult(HealthStatus.Degraded,
"No Authentik provider ID stored — SAML not provisioned");
}
if (!int.TryParse(instance.AuthentikProviderId, out var providerId))
{
return new HealthCheckResult(HealthStatus.Critical,
$"Invalid Authentik provider ID: {instance.AuthentikProviderId}");
}
try
{
var response = await _authentikClient.GetSamlProviderAsync(providerId);
if (response.IsSuccessStatusCode && response.Content is not null)
{
return new HealthCheckResult(HealthStatus.Healthy,
$"SAML provider {providerId} is active in Authentik");
}
return new HealthCheckResult(HealthStatus.Critical,
$"SAML provider {providerId} not found or inaccessible",
response.Error?.Content);
}
catch (Exception ex)
{
return new HealthCheckResult(HealthStatus.Critical,
$"Failed to check SAML provider: {ex.Message}");
}
}
}