Files
hrynco-notification-service/HrynCo.NotificationService.Services/PushChannels/SavePushChannelHandler.cs
T
agrynco 1631786ce2 feat(push): add service-owned firebase channels and delivery
Add push contracts, provider delivery and channel diagnostics. Link email channels to services and document migration and validation.

Ref: IT-1074
2026-10-24 12:31:35 +03:00

69 lines
3.0 KiB
C#

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<SavePushChannelCommand> logger, IPushChannelRepository channels)
: RequestHandler<SavePushChannelCommand, ServiceResult<Guid>>(logger)
{
protected override async Task<ServiceResult<Guid>> DoOnHandle(SavePushChannelCommand request, CancellationToken cancellationToken)
{
var existing = await channels.GetByIdAsync(request.Id, cancellationToken);
if (request.Id != Guid.Empty && existing is null)
return Failure<Guid>("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<Guid>("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<string?> 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<ServiceResult<Guid>> 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<Guid>(error.Message, error.IsConflict ? ServiceErrorCode.Conflict : ServiceErrorCode.InvalidRequest);
}
}
}