Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions backend/src/CCE.Api.External/Endpoints/DeviceTokenEndpoints.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using CCE.Api.Common.Extensions;
using CCE.Api.Common.Results;
using CCE.Application.Common.Interfaces;
using CCE.Application.Notifications.Public.Commands.RegisterDeviceToken;
using CCE.Application.Notifications.Public.Commands.UnregisterDeviceToken;
using CCE.Domain;
using MediatR;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;

namespace CCE.Api.External.Endpoints;

public static class DeviceTokenEndpoints
{
public static IEndpointRouteBuilder MapDeviceTokenEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/me/device-tokens")
.WithTags("Notifications")
.RequireAuthorization();

group.MapPost("", async (
RegisterDeviceTokenRequest body,
ICurrentUserAccessor currentUser,
IMediator mediator,
CancellationToken ct) =>
{
var userId = currentUser.GetUserId() ?? System.Guid.Empty;
if (userId == System.Guid.Empty) return EnvelopeResults.Unauthorized();
var cmd = new RegisterDeviceTokenCommand(userId, body.Token, body.Platform, body.DeviceId);
var result = await mediator.Send(cmd, ct).ConfigureAwait(false);
return result.ToHttpResult();
})
.WithName("RegisterDeviceToken")
.RequireAuthorization(Permissions.Notification_DeviceToken_Register);

group.MapDelete("/{deviceId}", async (
string deviceId,
ICurrentUserAccessor currentUser,
IMediator mediator,
CancellationToken ct) =>
{
var userId = currentUser.GetUserId() ?? System.Guid.Empty;
if (userId == System.Guid.Empty) return EnvelopeResults.Unauthorized();
var cmd = new UnregisterDeviceTokenCommand(userId, deviceId);
var result = await mediator.Send(cmd, ct).ConfigureAwait(false);
return result.ToHttpResult();
})
.WithName("UnregisterDeviceToken")
.RequireAuthorization(Permissions.Notification_DeviceToken_Delete);

return app;
}
}

public sealed record RegisterDeviceTokenRequest(
string Token,
string Platform,
string DeviceId);
1 change: 1 addition & 0 deletions backend/src/CCE.Api.External/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
app.MapAssetEndpoints();
app.MapAuthEndpoints(CCE.Application.Identity.Auth.Common.LocalAuthApi.External);
app.MapNotificationsEndpoints();
app.MapDeviceTokenEndpoints();
app.MapTagsPublicEndpoints();
app.MapSharePublicEndpoints();
app.MapNewsPublicEndpoints();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using CCE.Api.Common.Extensions;
using CCE.Application.Notifications.Admin.Commands.SendTestPush;
using CCE.Domain;
using MediatR;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing;

namespace CCE.Api.Internal.Endpoints;

public static class NotificationTestEndpoints
{
public static IEndpointRouteBuilder MapNotificationTestEndpoints(this IEndpointRouteBuilder app)
{
app.MapPost("/api/admin/notifications/test-push", async (
SendTestPushRequest body,
IMediator mediator,
CancellationToken ct) =>
{
var result = await mediator.Send(
new SendTestPushCommand(body.Token, body.Title, body.Body), ct)
.ConfigureAwait(false);
return result.ToHttpResult();
})
.WithTags("Notifications")
.WithName("SendTestPush")
.RequireAuthorization(Permissions.Notification_Send);

return app;
}
}

public sealed record SendTestPushRequest(string Token, string Title, string Body);
1 change: 1 addition & 0 deletions backend/src/CCE.Api.Internal/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
app.MapCommunityAdminEndpoints();
app.MapNotificationTemplateEndpoints();
app.MapNotificationLogEndpoints();
app.MapNotificationTestEndpoints();
app.MapReportEndpoints();
app.MapAuditEndpoints();
app.MapCacheManagementEndpoints();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using CCE.Application.Common;
using CCE.Application.Common;
using CCE.Application.Common.Interfaces;
using CCE.Application.Common.Pagination;
using CCE.Application.Messages;
Expand Down Expand Up @@ -41,7 +41,7 @@ public RetryNotificationLogCommandHandler(
var log = await _logRepository.GetAsync(request.Id, cancellationToken).ConfigureAwait(false);

if (log is null)
return _msg.NotificationLogNotFound<System.Guid>();
return _msg.NotFound<System.Guid>(MessageKeys.Notifications.NOTIFICATION_NOT_FOUND);

if (log.Status != NotificationDeliveryStatus.Failed && log.Status != NotificationDeliveryStatus.Skipped)
throw new DomainException($"Cannot retry a log with status {log.Status}.");
Expand All @@ -59,7 +59,7 @@ public RetryNotificationLogCommandHandler(
{
log.MarkSkipped("Template no longer available.");
await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
return _msg.NotificationRetried(log.Id);
return _msg.Ok(log.Id, MessageKeys.Notifications.NOTIFICATION_RETRIED);
}

// Resolve recipient data
Expand Down Expand Up @@ -110,7 +110,7 @@ public RetryNotificationLogCommandHandler(
{
log.MarkSkipped($"No sender registered for channel {log.Channel}.");
await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
return _msg.NotificationRetried(log.Id);
return _msg.Ok(log.Id, MessageKeys.Notifications.NOTIFICATION_RETRIED);
}

var sendResult = await sender.SendAsync(rendered, cancellationToken).ConfigureAwait(false);
Expand All @@ -126,6 +126,6 @@ public RetryNotificationLogCommandHandler(

await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);

return _msg.NotificationRetried(log.Id);
return _msg.Ok(log.Id, MessageKeys.Notifications.NOTIFICATION_RETRIED);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using CCE.Application.Common;
using MediatR;

namespace CCE.Application.Notifications.Admin.Commands.SendTestPush;

public sealed record SendTestPushCommand(string Token, string Title, string Body)
: IRequest<Response<TestPushResultDto>>;

public sealed record TestPushResultDto(int Sent, int Failed);
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using CCE.Application.Common;
using CCE.Application.Messages;
using MediatR;

namespace CCE.Application.Notifications.Admin.Commands.SendTestPush;

public sealed class SendTestPushCommandHandler
: IRequestHandler<SendTestPushCommand, Response<TestPushResultDto>>
{
private readonly IFirebasePushService _push;
private readonly MessageFactory _msg;

public SendTestPushCommandHandler(IFirebasePushService push, MessageFactory msg)
{
_push = push;
_msg = msg;
}

public async Task<Response<TestPushResultDto>> Handle(
SendTestPushCommand request, CancellationToken cancellationToken)
{
var (sent, failed) = await _push
.SendAsync(request.Token, request.Title, request.Body, cancellationToken)
.ConfigureAwait(false);
return _msg.Ok(new TestPushResultDto(sent, failed), MessageKeys.General.SUCCESS_OPERATION);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public async Task<Response<NotificationLogDto>> Handle(
.FirstOrDefault();

return log is null
? _msg.NotificationLogNotFound<NotificationLogDto>()
? _msg.NotFound<NotificationLogDto>(MessageKeys.Notifications.NOTIFICATION_NOT_FOUND)
: _msg.Ok(MapToDto(log), MessageKeys.General.ITEMS_LISTED);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using CCE.Application.Common;
using CCE.Application.Common;
using CCE.Application.Common.Interfaces;
using CCE.Application.Messages;
using CCE.Domain.Notifications;
Expand Down Expand Up @@ -39,6 +39,6 @@ public CreateNotificationTemplateCommandHandler(
await _repo.AddAsync(template, cancellationToken).ConfigureAwait(false);
await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);

return _msg.NotificationTemplateCreated(template.Id);
return _msg.Ok(template.Id, MessageKeys.Notifications.NOTIFICATION_TEMPLATE_CREATED);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using CCE.Application.Common;
using CCE.Application.Common;
using CCE.Application.Common.Interfaces;
using CCE.Application.Messages;
using MediatR;
Expand Down Expand Up @@ -29,7 +29,7 @@ public UpdateNotificationTemplateCommandHandler(
var template = await _repo.GetAsync(request.Id, cancellationToken).ConfigureAwait(false);
if (template is null)
{
return _msg.NotificationTemplateNotFound<System.Guid>();
return _msg.NotFound<System.Guid>(MessageKeys.Notifications.TEMPLATE_NOT_FOUND);
}

template.UpdateContent(request.SubjectAr, request.SubjectEn, request.BodyAr, request.BodyEn);
Expand All @@ -41,6 +41,6 @@ public UpdateNotificationTemplateCommandHandler(

await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);

return _msg.NotificationTemplateUpdated(template.Id);
return _msg.Ok(template.Id, MessageKeys.Notifications.NOTIFICATION_TEMPLATE_UPDATED);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ await _dispatcher.DispatchAsync(new NotificationMessage(
TemplateCode: "COUNTRY_CONTENT_REQUEST_APPROVED",
RecipientUserId: notification.RequestedById,
EventType: NotificationEventType.CountryResourceApproved,
Channels: [NotificationChannel.InApp, NotificationChannel.Email],
Channels: [NotificationChannel.InApp, NotificationChannel.Email, NotificationChannel.Push],
MetaData: new Dictionary<string, string>
{
["RequestId"] = notification.RequestId.ToString(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ await _dispatcher.DispatchAsync(new NotificationMessage(
TemplateCode: "COUNTRY_CONTENT_REQUEST_REJECTED",
RecipientUserId: notification.RequestedById,
EventType: NotificationEventType.CountryResourceRejected,
Channels: [NotificationChannel.InApp, NotificationChannel.Email],
Channels: [NotificationChannel.InApp, NotificationChannel.Email, NotificationChannel.Push],
MetaData: new Dictionary<string, string>
{
["RequestId"] = notification.RequestId.ToString(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ await _dispatcher.DispatchAsync(new NotificationMessage(
TemplateCode: MessageKeys.Identity.EXPERT_REQUEST_APPROVED,
RecipientUserId: notification.RequestedById,
EventType: NotificationEventType.ExpertRequestApproved,
Channels: [NotificationChannel.InApp, NotificationChannel.Email],
Channels: [NotificationChannel.InApp, NotificationChannel.Email, NotificationChannel.Push],
MetaData: new Dictionary<string, string>(),
Locale: "en"), cancellationToken).ConfigureAwait(false);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ await _dispatcher.DispatchAsync(new NotificationMessage(
TemplateCode: MessageKeys.Identity.EXPERT_REQUEST_REJECTED,
RecipientUserId: notification.RequestedById,
EventType: NotificationEventType.ExpertRequestRejected,
Channels: [NotificationChannel.InApp, NotificationChannel.Email],
Channels: [NotificationChannel.InApp, NotificationChannel.Email, NotificationChannel.Push],
MetaData: new Dictionary<string, string>
{
["Reason"] = notification.RejectionReasonEn ?? ""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ await _dispatcher.DispatchAsync(new NotificationMessage(
TemplateCode: "NEWS_PUBLISHED",
RecipientUserId: news.AuthorId,
EventType: NotificationEventType.NewsPublished,
Channels: [NotificationChannel.InApp],
Channels: [NotificationChannel.InApp, NotificationChannel.Push],
MetaData: new Dictionary<string, string>
{
["TitleAr"] = news.TitleAr,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ await _dispatcher.DispatchAsync(new NotificationMessage(
TemplateCode: "RESOURCE_PUBLISHED",
RecipientUserId: resource.UploadedById,
EventType: NotificationEventType.ResourcePublished,
Channels: [NotificationChannel.InApp],
Channels: [NotificationChannel.InApp, NotificationChannel.Push],
MetaData: new Dictionary<string, string>
{
["TitleAr"] = resource.TitleAr,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace CCE.Application.Notifications;

public interface IFirebasePushService
{
Task<(int Sent, int Failed)> SendAsync(string token, string title, string body, CancellationToken ct);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using CCE.Domain.Notifications;

namespace CCE.Application.Notifications;

public interface IUserDeviceTokenRepository
{
Task<System.Collections.Generic.IReadOnlyList<UserDeviceToken>> GetActiveByUserIdAsync(
System.Guid userId, CancellationToken cancellationToken);

Task<UserDeviceToken?> GetByUserAndDeviceAsync(
System.Guid userId, string deviceId, CancellationToken cancellationToken);

Task AddAsync(UserDeviceToken token, CancellationToken cancellationToken);

/// <summary>Deactivates tokens matching the given FCM token values after FCM rejects them.</summary>
Task DeactivateByTokensAsync(
System.Collections.Generic.IReadOnlyList<string> fcmTokens, CancellationToken cancellationToken);
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using CCE.Application.Common;
using CCE.Application.Common;
using CCE.Application.Messages;
using CCE.Application.Notifications.Public;
using CCE.Domain.Common;
Expand Down Expand Up @@ -28,6 +28,6 @@ public async Task<Response<int>> Handle(MarkAllNotificationsReadCommand request,
request.UserId,
_clock,
cancellationToken).ConfigureAwait(false);
return _msg.NotificationsMarkedRead(count);
return _msg.Ok(count, MessageKeys.Notifications.NOTIFICATIONS_MARKED_READ);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using CCE.Application.Common;
using CCE.Application.Common;
using CCE.Application.Common.Interfaces;
using CCE.Application.Community;
using CCE.Application.Messages;
Expand Down Expand Up @@ -35,7 +35,7 @@ public async Task<Response<VoidData>> Handle(MarkNotificationReadCommand request
var notif = await _repo.GetAsync(request.Id, cancellationToken).ConfigureAwait(false);

if (notif is null || notif.UserId != request.UserId)
return _msg.NotificationLogNotFound<VoidData>();
return _msg.NotFound<VoidData>(MessageKeys.Notifications.NOTIFICATION_NOT_FOUND);

notif.MarkRead(_clock);
await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
Expand All @@ -45,6 +45,6 @@ public async Task<Response<VoidData>> Handle(MarkNotificationReadCommand request
await _feedStore.IncrementNotificationCountAsync(notif.UserId, delta: -1, cancellationToken)
.ConfigureAwait(false);

return _msg.NotificationMarkedRead();
return _msg.Ok(MessageKeys.Notifications.NOTIFICATION_MARKED_READ);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using CCE.Application.Common;
using MediatR;

namespace CCE.Application.Notifications.Public.Commands.RegisterDeviceToken;

public sealed record RegisterDeviceTokenCommand(
System.Guid UserId,
string Token,
string Platform,
string DeviceId
) : IRequest<Response<VoidData>>;
Loading