using Microsoft.Data.Sqlite; namespace MxGateway.Server.Security.Authentication; public sealed class SqliteApiKeyAdminStore(AuthSqliteConnectionFactory connectionFactory) : IApiKeyAdminStore { public async Task CreateAsync(ApiKeyCreateRequest request, CancellationToken cancellationToken) { await using SqliteConnection connection = connectionFactory.CreateConnection(); await connection.OpenAsync(cancellationToken).ConfigureAwait(false); await using SqliteCommand command = connection.CreateCommand(); command.CommandText = """ INSERT INTO api_keys ( key_id, key_prefix, secret_hash, display_name, scopes, created_utc, last_used_utc, revoked_utc) VALUES ( $key_id, $key_prefix, $secret_hash, $display_name, $scopes, $created_utc, NULL, NULL); """; AddCreateParameters(command, request); await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); } public async Task> ListAsync(CancellationToken cancellationToken) { await using SqliteConnection connection = connectionFactory.CreateConnection(); await connection.OpenAsync(cancellationToken).ConfigureAwait(false); await using SqliteCommand command = connection.CreateCommand(); command.CommandText = """ SELECT key_id, key_prefix, secret_hash, display_name, scopes, created_utc, last_used_utc, revoked_utc FROM api_keys ORDER BY key_id; """; List records = []; await using SqliteDataReader reader = await command.ExecuteReaderAsync(cancellationToken) .ConfigureAwait(false); while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false)) { records.Add(ApiKeyRecordReader.Read(reader)); } return records; } public async Task RevokeAsync(string keyId, DateTimeOffset revokedUtc, CancellationToken cancellationToken) { await using SqliteConnection connection = connectionFactory.CreateConnection(); await connection.OpenAsync(cancellationToken).ConfigureAwait(false); await using SqliteCommand command = connection.CreateCommand(); command.CommandText = """ UPDATE api_keys SET revoked_utc = $revoked_utc WHERE key_id = $key_id AND revoked_utc IS NULL; """; command.Parameters.AddWithValue("$key_id", keyId); command.Parameters.AddWithValue("$revoked_utc", revokedUtc.ToString("O")); int rows = await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); return rows > 0; } public async Task RotateAsync( string keyId, byte[] secretHash, DateTimeOffset rotatedUtc, CancellationToken cancellationToken) { await using SqliteConnection connection = connectionFactory.CreateConnection(); await connection.OpenAsync(cancellationToken).ConfigureAwait(false); await using SqliteCommand command = connection.CreateCommand(); command.CommandText = """ UPDATE api_keys SET secret_hash = $secret_hash, last_used_utc = NULL, revoked_utc = NULL WHERE key_id = $key_id; """; command.Parameters.AddWithValue("$key_id", keyId); command.Parameters.Add("$secret_hash", SqliteType.Blob).Value = secretHash; int rows = await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); return rows > 0; } private static void AddCreateParameters(SqliteCommand command, ApiKeyCreateRequest request) { command.Parameters.AddWithValue("$key_id", request.KeyId); command.Parameters.AddWithValue("$key_prefix", request.KeyPrefix); command.Parameters.Add("$secret_hash", SqliteType.Blob).Value = request.SecretHash; command.Parameters.AddWithValue("$display_name", request.DisplayName); command.Parameters.AddWithValue("$scopes", ApiKeyScopeSerializer.Serialize(request.Scopes)); command.Parameters.AddWithValue("$created_utc", request.CreatedUtc.ToString("O")); } }