using System.Data;
using Microsoft.Data.SqlClient;
using ZB.MOM.WW.Auth.Abstractions.ApiKeys;
namespace ZB.MOM.WW.Auth.ApiKeys.SqlServer;
///
/// SQL Server-backed, append-only audit store for API-key events. A one-for-one port of
/// SqliteApiKeyAuditStore; on a clustered host the rows from every node land in one table,
/// so the audit trail no longer has to be stitched together per node.
///
/// Factory for connections to the shared API-key database.
public sealed class SqlServerApiKeyAuditStore(AuthSqlServerConnectionFactory connectionFactory) : IApiKeyAuditStore
{
///
public async Task AppendAsync(ApiKeyAuditEntry entry, CancellationToken ct)
{
ArgumentNullException.ThrowIfNull(entry);
string schema = connectionFactory.Schema;
await using SqlConnection connection =
await connectionFactory.OpenConnectionAsync(ct).ConfigureAwait(false);
await using SqlCommand command = connectionFactory.CreateCommand(connection, $"""
INSERT INTO [{schema}].[{SqlServerAuthSchema.ApiKeyAuditTable}] (
key_id, event_type, remote_address, created_utc, details)
VALUES (@key_id, @event_type, @remote_address, @created_utc, @details);
""");
command.Parameters.Add("@key_id", SqlDbType.NVarChar, 64).Value = (object?)entry.KeyId ?? DBNull.Value;
command.Parameters.Add("@event_type", SqlDbType.NVarChar, 64).Value = entry.EventType;
command.Parameters.Add("@remote_address", SqlDbType.NVarChar, 64).Value = (object?)entry.RemoteAddress ?? DBNull.Value;
command.Parameters.Add("@created_utc", SqlDbType.DateTimeOffset).Value = entry.CreatedUtc;
command.Parameters.Add("@details", SqlDbType.NVarChar, -1).Value = (object?)entry.Details ?? DBNull.Value;
await command.ExecuteNonQueryAsync(ct).ConfigureAwait(false);
}
///
///
/// Ordered by the IDENTITY audit_id, not by created_utc: two events stamped in the
/// same instant still come back in insertion order. A non-positive limit short-circuits without
/// opening a connection — SELECT TOP (-1) is not even legal T-SQL.
///
public async Task> ListRecentAsync(int limit, CancellationToken ct)
{
if (limit <= 0)
{
return [];
}
string schema = connectionFactory.Schema;
await using SqlConnection connection =
await connectionFactory.OpenConnectionAsync(ct).ConfigureAwait(false);
await using SqlCommand command = connectionFactory.CreateCommand(connection, $"""
SELECT TOP (@limit) key_id, event_type, remote_address, created_utc, details
FROM [{schema}].[{SqlServerAuthSchema.ApiKeyAuditTable}]
ORDER BY audit_id DESC;
""");
command.Parameters.Add("@limit", SqlDbType.Int).Value = limit;
List entries = [];
await using SqlDataReader reader = await command.ExecuteReaderAsync(ct).ConfigureAwait(false);
while (await reader.ReadAsync(ct).ConfigureAwait(false))
{
entries.Add(new ApiKeyAuditEntry(
KeyId: reader.IsDBNull(0) ? null : reader.GetString(0),
EventType: reader.GetString(1),
RemoteAddress: reader.IsDBNull(2) ? null : reader.GetString(2),
CreatedUtc: reader.GetDateTimeOffset(3),
Details: reader.IsDBNull(4) ? null : reader.GetString(4)));
}
return entries;
}
}