using System.Security.Cryptography; using System.Text.Json; using HrynCo.NotificationService.DAL.Abstract.PushChannels; using HrynCo.NotificationService.Services.Core; using HrynCo.NotificationService.Services.Logging; using static HrynCo.NotificationService.Services.Core.ServiceResultHelper; namespace HrynCo.NotificationService.Services.PushChannels; internal sealed class SavePushChannelHandler( IContextualSerilogLogger logger, IPushChannelRepository channels) : RequestHandler>(logger) { protected override async Task> DoOnHandle(SavePushChannelCommand request, CancellationToken cancellationToken) { var existing = await channels.GetByIdAsync(request.Id, cancellationToken); if (request.Id != Guid.Empty && existing is null) return Failure("Push channel not found.", ServiceErrorCode.NotFound); var projectId = request.FirebaseProjectId.Trim(); var credentials = await ResolveCredentialsAsync(request, existing, projectId, cancellationToken); if (!ValidCredentials(credentials, projectId)) return Failure("Provide a valid Firebase service account JSON for this project. Changing project requires new credentials.", ServiceErrorCode.InvalidRequest); var channel = new PushChannel { Id = existing?.Id ?? Guid.NewGuid(), ServiceId = request.ServiceId, FirebaseProjectId = projectId, IsActive = request.IsActive }; return await SaveAsync(channel, credentials!, existing is null, cancellationToken); } private async Task ResolveCredentialsAsync(SavePushChannelCommand request, PushChannel? existing, string projectId, CancellationToken ct) { if (!string.IsNullOrWhiteSpace(request.CredentialsJson)) return request.CredentialsJson; if (existing is null || existing.FirebaseProjectId != projectId) return null; return await channels.GetCredentialsAsync(existing.Id, ct); } private static bool ValidCredentials(string? json, string projectId) { if (string.IsNullOrWhiteSpace(json)) return false; try { FirebaseServiceAccountCredentials.Parse(json, projectId); return true; } catch (Exception error) when (error is JsonException or FormatException or ArgumentException or CryptographicException) { // Neither parser exceptions nor submitted JSON are sent to logs or clients. return false; } } private async Task> SaveAsync(PushChannel channel, string credentials, bool isNew, CancellationToken ct) { try { await channels.SaveAsync(channel, credentials, isNew, ct); return Success(channel.Id); } catch (PushChannelWriteException error) { return Failure(error.Message, error.IsConflict ? ServiceErrorCode.Conflict : ServiceErrorCode.InvalidRequest); } } }