using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.Auth.Abstractions.ApiKeys;
using ZB.MOM.WW.Auth.ApiKeys.DependencyInjection;
namespace ZB.MOM.WW.Auth.ApiKeys.SqlServer.DependencyInjection;
///
/// Dependency-injection entry point that swaps the API-key store backend from the SQLite default
/// to a shared SQL Server database — one row set read and written by every node of a cluster (e.g.
/// a ScadaBridge central pair), instead of the per-node file alone
/// would give each node.
///
public static class SqlServerApiKeyServiceCollectionExtensions
{
///
/// Registers the SQL Server-backed , ,
/// and , then calls
/// to complete the registration
/// (verifier, pepper provider, migration hosted service).
///
///
///
/// Call this INSTEAD of ,
/// never both — this method calls it internally. The SQL Server stores and migrator are
/// registered here, BEFORE that inner call, precisely because
/// registers everything with
/// TryAdd*: whichever implementation of (etc.) is registered
/// first wins, and a later TryAdd for the same service is a no-op. That ordering is the
/// entire mechanism by which this method displaces the SQLite default.
///
///
/// still registers a SQLite
/// connection factory from ApiKeyOptions.SqlitePath even when SQL Server wins every
/// store slot. That is harmless — the SQLite factory is lazy and the connection it would open
/// is never opened when nothing resolves it — and it is actually useful: it keeps the one path
/// a node's pre-existing per-node SQLite file lives at in a single option, which is exactly the
/// source path needs for the one-shot copy into the
/// shared store.
///
///
/// is registered here so a host can resolve it and run
/// it once at boot (e.g. from a startup task or its own hosted service) to copy an existing
/// node's per-node keys and audit rows into the now-shared store; it performs no work by being
/// merely registered.
///
///
/// The connection string must be a literal value or an environment-variable override — never
/// a ${secret:} reference on a host whose secret store is itself SQL-backed: resolving
/// that reference would require a working secret store, which on such a host requires this very
/// database to already be reachable, an unbreakable circular dependency at boot.
///
///
/// The service collection to add to.
/// The application configuration.
/// Path of the configuration section holding the core API-key options.
/// Path of the configuration section holding .
/// The same instance, for chaining.
public static IServiceCollection AddZbApiKeyAuthSqlServer(
this IServiceCollection services,
IConfiguration config,
string apiKeySectionPath,
string sqlServerSectionPath)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(config);
ArgumentException.ThrowIfNullOrWhiteSpace(apiKeySectionPath);
ArgumentException.ThrowIfNullOrWhiteSpace(sqlServerSectionPath);
services.Configure(config.GetSection(sqlServerSectionPath));
services.TryAddSingleton(sp =>
new AuthSqlServerConnectionFactory(sp.GetRequiredService>().Value));
// Registered BEFORE AddZbApiKeyAuth so its TryAdd calls find these already present and
// leave them alone — that is the whole mechanism by which the SQL Server store displaces
// SQLite (see the class remarks).
services.TryAddSingleton(sp =>
new SqlServerApiKeyStore(sp.GetRequiredService()));
services.TryAddSingleton(sp =>
new SqlServerApiKeyAdminStore(sp.GetRequiredService()));
services.TryAddSingleton(sp =>
new SqlServerApiKeyAuditStore(sp.GetRequiredService()));
services.TryAddSingleton();
services.TryAddSingleton(sp => sp.GetRequiredService());
// Registered so a host can resolve it and run the one-shot per-node SQLite -> shared store
// copy at boot; registering it here does no work on its own.
services.TryAddSingleton();
return services.AddZbApiKeyAuth(config, apiKeySectionPath);
}
}