using SurrealDb.Net; using TarninaHub.Application.Abstractions; using TarninaHub.Application.Dtos; namespace TarninaHub.Infrastructure.Persistence; public sealed class ConsentService : IConsentService { private const string SelectConsentFields = "type::string(id) AS id, type::string(user) AS user, scope, granted_at, expires_at, revoked_at"; private readonly ISurrealDbClient _client; public ConsentService(ISurrealDbClient client) { _client = client; } public async Task GrantAsync( string userId, GrantConsentRequest request, CancellationToken cancellationToken) { var (userTable, userIdPart) = SurrealRecordId.Split(userId); var consentId = Guid.NewGuid().ToString("N"); var expiresClause = request.ExpiresAt.HasValue ? ", expires_at = $expiresAt" : string.Empty; var parameters = new Dictionary { ["consentId"] = consentId, ["userTable"] = userTable, ["userId"] = userIdPart, ["scope"] = request.Scope, }; if (request.ExpiresAt.HasValue) { parameters["expiresAt"] = request.ExpiresAt; } var response = await _client.RawQuery( "CREATE type::record('consent', $consentId) SET " + "user = type::record($userTable, $userId), scope = $scope, " + "granted_at = time::now()" + expiresClause + "; " + $"SELECT {SelectConsentFields} FROM type::record('consent', $consentId) LIMIT 1;", parameters, cancellationToken); response.EnsureAllOks(); var rows = response.GetValue>(1) ?? []; return Map(rows[0]); } public async Task> ListForUserAsync( string userId, CancellationToken cancellationToken) { var (userTable, userIdPart) = SurrealRecordId.Split(userId); var response = await _client.RawQuery( $"SELECT {SelectConsentFields} FROM consent " + "WHERE user = type::record($userTable, $userId) ORDER BY granted_at DESC;", new Dictionary { ["userTable"] = userTable, ["userId"] = userIdPart, }, cancellationToken); response.EnsureAllOks(); var rows = response.GetValue>(0) ?? []; return rows.Select(Map).ToList(); } public async Task RevokeAsync( string consentId, string userId, CancellationToken cancellationToken) { var (userTable, userIdPart) = SurrealRecordId.Split(userId); var (consentTable, consentIdPart) = SurrealRecordId.Split(consentId); var response = await _client.RawQuery( "UPDATE type::record($consentTable, $consentIdPart) SET revoked_at = time::now() " + "WHERE user = type::record($userTable, $userId) AND revoked_at = NONE; " + $"SELECT {SelectConsentFields} FROM type::record($consentTable, $consentIdPart) LIMIT 1;", new Dictionary { ["consentTable"] = consentTable, ["consentIdPart"] = consentIdPart, ["userTable"] = userTable, ["userId"] = userIdPart, }, cancellationToken); response.EnsureAllOks(); var rows = response.GetValue>(1) ?? []; return rows.Count > 0 && rows[0].RevokedAt is not null; } private static ConsentResponse Map(ConsentRow row) => new( row.Id, row.User, row.Scope, row.GrantedAt, row.ExpiresAt, row.RevokedAt); }