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
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
using HrynCo.NotificationService.Services.Core;
|
||||
using MediatR;
|
||||
|
||||
namespace HrynCo.NotificationService.Services.PushChannels;
|
||||
|
||||
// A class deliberately avoids record-generated ToString exposing the submitted secret.
|
||||
public sealed class SavePushChannelCommand : IRequest<ServiceResult<Guid>>
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
public Guid ServiceId { get; init; }
|
||||
public required string FirebaseProjectId { get; init; }
|
||||
public string? CredentialsJson { get; init; }
|
||||
public bool IsActive { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user