using Microsoft.Data.Sqlite; namespace MxGateway.Server.Security.Authentication; public sealed class SqliteApiKeyStore(AuthSqliteConnectionFactory connectionFactory) : IApiKeyStore { public Task FindByKeyIdAsync(string keyId, CancellationToken cancellationToken) { return FindByKeyIdAsync(keyId, requireActive: false, cancellationToken); } public Task FindActiveByKeyIdAsync(string keyId, CancellationToken cancellationToken) { return FindByKeyIdAsync(keyId, requireActive: true, cancellationToken); } public async Task MarkKeyUsedAsync(string keyId, DateTimeOffset usedUtc, 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 last_used_utc = $last_used_utc WHERE key_id = $key_id AND revoked_utc IS NULL; """; command.Parameters.AddWithValue("$key_id", keyId); command.Parameters.AddWithValue("$last_used_utc", usedUtc.ToString("O")); await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); } private async Task FindByKeyIdAsync( string keyId, bool requireActive, CancellationToken cancellationToken) { await using SqliteConnection connection = connectionFactory.CreateConnection(); await connection.OpenAsync(cancellationToken).ConfigureAwait(false); await using SqliteCommand command = connection.CreateCommand(); command.CommandText = requireActive ? """ SELECT key_id, key_prefix, secret_hash, display_name, scopes, created_utc, last_used_utc, revoked_utc FROM api_keys WHERE key_id = $key_id AND revoked_utc IS NULL; """ : """ SELECT key_id, key_prefix, secret_hash, display_name, scopes, created_utc, last_used_utc, revoked_utc FROM api_keys WHERE key_id = $key_id; """; command.Parameters.AddWithValue("$key_id", keyId); await using SqliteDataReader reader = await command.ExecuteReaderAsync(cancellationToken) .ConfigureAwait(false); if (!await reader.ReadAsync(cancellationToken).ConfigureAwait(false)) { return null; } return ReadApiKeyRecord(reader); } private static ApiKeyRecord ReadApiKeyRecord(SqliteDataReader reader) { return new ApiKeyRecord( KeyId: reader.GetString(0), KeyPrefix: reader.GetString(1), SecretHash: (byte[])reader["secret_hash"], DisplayName: reader.GetString(3), Scopes: ApiKeyScopeSerializer.Deserialize(reader.GetString(4)), CreatedUtc: DateTimeOffset.Parse(reader.GetString(5), System.Globalization.CultureInfo.InvariantCulture), LastUsedUtc: ReadNullableDateTimeOffset(reader, 6), RevokedUtc: ReadNullableDateTimeOffset(reader, 7)); } private static DateTimeOffset? ReadNullableDateTimeOffset(SqliteDataReader reader, int ordinal) { return reader.IsDBNull(ordinal) ? null : DateTimeOffset.Parse(reader.GetString(ordinal), System.Globalization.CultureInfo.InvariantCulture); } }