Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PM-10600] Push notification with full notification center content #5086

Merged
merged 27 commits into from
Feb 12, 2025
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
a93d52a
PM-10600: Notification push notification
mzieniukbw Oct 21, 2024
00a9829
PM-10600: Sending to specific client types for relay push notifications
mzieniukbw Oct 22, 2024
2a07904
PM-10600: Sending to specific client types for other clients
mzieniukbw Oct 22, 2024
8ea69e8
PM-10600: Send push notification on notification creation
mzieniukbw Oct 22, 2024
28d5df9
PM-10600: Explicit group names
mzieniukbw Oct 22, 2024
61e1053
PM-10600: Id typos
mzieniukbw Oct 22, 2024
41b45e5
PM-10600: Revert global push notifications
mzieniukbw Oct 22, 2024
cc2d2c3
PM-10600: Added DeviceType claim
mzieniukbw Oct 22, 2024
5b46b56
PM-10600: Sent to organization typo
mzieniukbw Oct 22, 2024
b9ecf86
PM-10600: UT coverage
mzieniukbw Oct 22, 2024
ed810ea
PM-10600: Small refactor, UTs coverage
mzieniukbw Oct 22, 2024
5c09ca8
PM-10600: UTs coverage
mzieniukbw Oct 23, 2024
a88be80
PM-10600: Startup fix
mzieniukbw Oct 23, 2024
99ca476
PM-10600: Test fix
mzieniukbw Oct 23, 2024
07ec392
PM-10600: Required attribute, organization group for push notificatioโ€ฆ
mzieniukbw Oct 24, 2024
f1fd047
PM-10600: UT coverage
mzieniukbw Oct 24, 2024
66f3500
PM-10600: Fix Mobile devices not registering to organization push notโ€ฆ
mzieniukbw Nov 5, 2024
83aa75f
PM-10600: Unit Test coverage for NotificationHubPushRegistrationService
mzieniukbw Nov 5, 2024
7bdfd92
PM-10600: Unit Tests fix to NotificationHubPushRegistrationService afโ€ฆ
mzieniukbw Nov 20, 2024
41b1d64
PM-10600: Organization push notifications not sending to mobile devicโ€ฆ
mzieniukbw Nov 20, 2024
e9e33c0
PM-10600: Fix self-hosted organization notification not being receiveโ€ฆ
mzieniukbw Nov 20, 2024
3bc6025
PM-10600: Broken NotificationsController integration test
mzieniukbw Dec 18, 2024
246ca91
PM-10600: Merge conflicts fix
mzieniukbw Jan 13, 2025
abdd064
Merge branch 'main' into km/pm-10600
mzieniukbw Jan 15, 2025
4a7b99d
merge conflict fix
mzieniukbw Jan 15, 2025
02e45c3
PM-10600: Push notification with full notification center content.
mzieniukbw Nov 26, 2024
7e019d1
Merge branch 'main' into km/pm-10600-full-notification-content
mzieniukbw Feb 12, 2025
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
6 changes: 3 additions & 3 deletions src/Api/Platform/Push/Controllers/PushController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public async Task PostRegister([FromBody] PushRegistrationRequestModel model)
{
CheckUsage();
await _pushRegistrationService.CreateOrUpdateRegistrationAsync(model.PushToken, Prefix(model.DeviceId),
Prefix(model.UserId), Prefix(model.Identifier), model.Type);
Prefix(model.UserId), Prefix(model.Identifier), model.Type, model.OrganizationIds.Select(Prefix));
}

[HttpPost("delete")]
Expand Down Expand Up @@ -79,12 +79,12 @@ public async Task PostSend([FromBody] PushSendRequestModel model)
if (!string.IsNullOrWhiteSpace(model.UserId))
{
await _pushNotificationService.SendPayloadToUserAsync(Prefix(model.UserId),
model.Type.Value, model.Payload, Prefix(model.Identifier), Prefix(model.DeviceId));
model.Type, model.Payload, Prefix(model.Identifier), Prefix(model.DeviceId), model.ClientType);
}
else if (!string.IsNullOrWhiteSpace(model.OrganizationId))
{
await _pushNotificationService.SendPayloadToOrganizationAsync(Prefix(model.OrganizationId),
model.Type.Value, model.Payload, Prefix(model.Identifier), Prefix(model.DeviceId));
model.Type, model.Payload, Prefix(model.Identifier), Prefix(model.DeviceId), model.ClientType);
}
}

Expand Down
5 changes: 5 additions & 0 deletions src/Core/Context/CurrentContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,11 @@ public virtual Task SetContextAsync(ClaimsPrincipal user)

DeviceIdentifier = GetClaimValue(claimsDict, Claims.Device);

if (Enum.TryParse(GetClaimValue(claimsDict, Claims.DeviceType), out DeviceType deviceType))
{
DeviceType = deviceType;
}

Organizations = GetOrganizations(claimsDict, orgApi);

Providers = GetProviders(claimsDict);
Expand Down
2 changes: 2 additions & 0 deletions src/Core/Enums/PushType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,6 @@ public enum PushType : byte
SyncOrganizations = 17,
SyncOrganizationStatusChanged = 18,
SyncOrganizationCollectionSettingChanged = 19,

SyncNotification = 20,
}
1 change: 1 addition & 0 deletions src/Core/Identity/Claims.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ public static class Claims
public const string SecurityStamp = "sstamp";
public const string Premium = "premium";
public const string Device = "device";
public const string DeviceType = "devicetype";

public const string OrganizationOwner = "orgowner";
public const string OrganizationAdmin = "orgadmin";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ public class PushRegistrationRequestModel
public DeviceType Type { get; set; }
[Required]
public string Identifier { get; set; }
public IEnumerable<string> OrganizationIds { get; set; }
}
18 changes: 9 additions & 9 deletions src/Core/Models/Api/Request/PushSendRequestModel.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
๏ปฟusing System.ComponentModel.DataAnnotations;
๏ปฟ#nullable enable
using System.ComponentModel.DataAnnotations;
using Bit.Core.Enums;

namespace Bit.Core.Models.Api;

public class PushSendRequestModel : IValidatableObject
{
public string UserId { get; set; }
public string OrganizationId { get; set; }
public string DeviceId { get; set; }
public string Identifier { get; set; }
[Required]
public PushType? Type { get; set; }
[Required]
public object Payload { get; set; }
public string? UserId { get; set; }
public string? OrganizationId { get; set; }
public string? DeviceId { get; set; }
public string? Identifier { get; set; }
public required PushType Type { get; set; }
public required object Payload { get; set; }
public ClientType? ClientType { get; set; }

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
Expand Down
17 changes: 17 additions & 0 deletions src/Core/Models/PushNotification.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
๏ปฟusing Bit.Core.Enums;
using Bit.Core.NotificationCenter.Enums;

namespace Bit.Core.Models;

Expand Down Expand Up @@ -45,6 +46,22 @@ public class SyncSendPushNotification
public DateTime RevisionDate { get; set; }
}

#nullable enable
public class NotificationPushNotification
{
public Guid Id { get; set; }
public Priority Priority { get; set; }
public bool Global { get; set; }
public ClientType ClientType { get; set; }
public Guid? UserId { get; set; }
public Guid? OrganizationId { get; set; }
public string? Title { get; set; }
public string? Body { get; set; }
public DateTime CreationDate { get; set; }
public DateTime RevisionDate { get; set; }
}
#nullable disable

public class AuthRequestPushNotification
{
public Guid UserId { get; set; }
Expand Down
12 changes: 10 additions & 2 deletions src/Core/NotificationCenter/Commands/CreateNotificationCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Bit.Core.NotificationCenter.Commands.Interfaces;
using Bit.Core.NotificationCenter.Entities;
using Bit.Core.NotificationCenter.Repositories;
using Bit.Core.Platform.Push;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization;

Expand All @@ -14,14 +15,17 @@ public class CreateNotificationCommand : ICreateNotificationCommand
private readonly ICurrentContext _currentContext;
private readonly IAuthorizationService _authorizationService;
private readonly INotificationRepository _notificationRepository;
private readonly IPushNotificationService _pushNotificationService;

public CreateNotificationCommand(ICurrentContext currentContext,
IAuthorizationService authorizationService,
INotificationRepository notificationRepository)
INotificationRepository notificationRepository,
IPushNotificationService pushNotificationService)
{
_currentContext = currentContext;
_authorizationService = authorizationService;
_notificationRepository = notificationRepository;
_pushNotificationService = pushNotificationService;
}

public async Task<Notification> CreateAsync(Notification notification)
Expand All @@ -31,6 +35,10 @@ public async Task<Notification> CreateAsync(Notification notification)
await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, notification,
NotificationOperations.Create);

return await _notificationRepository.CreateAsync(notification);
var newNotification = await _notificationRepository.CreateAsync(notification);

await _pushNotificationService.PushNotificationAsync(newNotification);

return newNotification;
}
}
5 changes: 2 additions & 3 deletions src/Core/NotificationCenter/Entities/Notification.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,8 @@ public class Notification : ITableObject<Guid>
public ClientType ClientType { get; set; }
public Guid? UserId { get; set; }
public Guid? OrganizationId { get; set; }
[MaxLength(256)]
public string? Title { get; set; }
public string? Body { get; set; }
[MaxLength(256)] public string? Title { get; set; }
[MaxLength(3000)] public string? Body { get; set; }
public DateTime CreationDate { get; set; }
public DateTime RevisionDate { get; set; }

Expand Down
2 changes: 1 addition & 1 deletion src/Core/NotificationHub/INotificationHubPool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ namespace Bit.Core.NotificationHub;

public interface INotificationHubPool
{
NotificationHubClient ClientFor(Guid comb);
INotificationHubClient ClientFor(Guid comb);
INotificationHubProxy AllClients { get; }
}
2 changes: 1 addition & 1 deletion src/Core/NotificationHub/NotificationHubPool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ private List<NotificationHubConnection> FilterInvalidHubs(IEnumerable<GlobalSett
/// <param name="comb"></param>
/// <returns></returns>
/// <exception cref="InvalidOperationException">Thrown when no notification hub is found for a given comb.</exception>
public NotificationHubClient ClientFor(Guid comb)
public INotificationHubClient ClientFor(Guid comb)
{
var possibleConnections = _connections.Where(c => c.RegistrationEnabled(comb)).ToArray();
if (possibleConnections.Length == 0)
Expand Down
77 changes: 54 additions & 23 deletions src/Core/NotificationHub/NotificationHubPushNotificationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using Bit.Core.Vault.Entities;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Notification = Bit.Core.NotificationCenter.Entities.Notification;

namespace Bit.Core.NotificationHub;

Expand Down Expand Up @@ -135,11 +136,7 @@ public async Task PushLogOutAsync(Guid userId, bool excludeCurrentContext = fals

private async Task PushUserAsync(Guid userId, PushType type, bool excludeCurrentContext = false)
{
var message = new UserPushNotification
{
UserId = userId,
Date = DateTime.UtcNow
};
var message = new UserPushNotification { UserId = userId, Date = DateTime.UtcNow };

await SendPayloadToUserAsync(userId, type, message, excludeCurrentContext);
}
Expand Down Expand Up @@ -184,31 +181,59 @@ public async Task PushAuthRequestResponseAsync(AuthRequest authRequest)
await PushAuthRequestAsync(authRequest, PushType.AuthRequestResponse);
}

private async Task PushAuthRequestAsync(AuthRequest authRequest, PushType type)
public async Task PushNotificationAsync(Notification notification)
{
var message = new AuthRequestPushNotification
var message = new NotificationPushNotification
{
Id = authRequest.Id,
UserId = authRequest.UserId
Id = notification.Id,
Priority = notification.Priority,
Global = notification.Global,
ClientType = notification.ClientType,
UserId = notification.UserId,
OrganizationId = notification.OrganizationId,
Title = notification.Title,
Body = notification.Body,
CreationDate = notification.CreationDate,
RevisionDate = notification.RevisionDate
};

if (notification.UserId.HasValue)
{
await SendPayloadToUserAsync(notification.UserId.Value, PushType.SyncNotification, message, true,
notification.ClientType);
}
else if (notification.OrganizationId.HasValue)
{
await SendPayloadToOrganizationAsync(notification.OrganizationId.Value, PushType.SyncNotification, message,
true, notification.ClientType);
}
}

private async Task PushAuthRequestAsync(AuthRequest authRequest, PushType type)
{
var message = new AuthRequestPushNotification { Id = authRequest.Id, UserId = authRequest.UserId };

await SendPayloadToUserAsync(authRequest.UserId, type, message, true);
}

private async Task SendPayloadToUserAsync(Guid userId, PushType type, object payload, bool excludeCurrentContext)
private async Task SendPayloadToUserAsync(Guid userId, PushType type, object payload, bool excludeCurrentContext,
ClientType? clientType = null)
{
await SendPayloadToUserAsync(userId.ToString(), type, payload, GetContextIdentifier(excludeCurrentContext));
await SendPayloadToUserAsync(userId.ToString(), type, payload, GetContextIdentifier(excludeCurrentContext),
clientType: clientType);
}

private async Task SendPayloadToOrganizationAsync(Guid orgId, PushType type, object payload, bool excludeCurrentContext)
private async Task SendPayloadToOrganizationAsync(Guid orgId, PushType type, object payload,
bool excludeCurrentContext, ClientType? clientType = null)
{
await SendPayloadToUserAsync(orgId.ToString(), type, payload, GetContextIdentifier(excludeCurrentContext));
await SendPayloadToOrganizationAsync(orgId.ToString(), type, payload,
GetContextIdentifier(excludeCurrentContext), clientType: clientType);
}

public async Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier,
string deviceId = null)
string deviceId = null, ClientType? clientType = null)
{
var tag = BuildTag($"template:payload_userId:{SanitizeTagInput(userId)}", identifier);
var tag = BuildTag($"template:payload_userId:{SanitizeTagInput(userId)}", identifier, clientType);
await SendPayloadAsync(tag, type, payload);
if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId))
{
Expand All @@ -217,9 +242,9 @@ public async Task SendPayloadToUserAsync(string userId, PushType type, object pa
}

public async Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier,
string deviceId = null)
string deviceId = null, ClientType? clientType = null)
{
var tag = BuildTag($"template:payload && organizationId:{SanitizeTagInput(orgId)}", identifier);
var tag = BuildTag($"template:payload && organizationId:{SanitizeTagInput(orgId)}", identifier, clientType);
await SendPayloadAsync(tag, type, payload);
if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId))
{
Expand Down Expand Up @@ -258,18 +283,23 @@ private string GetContextIdentifier(bool excludeCurrentContext)
return null;
}

var currentContext = _httpContextAccessor?.HttpContext?.
RequestServices.GetService(typeof(ICurrentContext)) as ICurrentContext;
var currentContext =
_httpContextAccessor?.HttpContext?.RequestServices.GetService(typeof(ICurrentContext)) as ICurrentContext;
return currentContext?.DeviceIdentifier;
}

private string BuildTag(string tag, string identifier)
private string BuildTag(string tag, string identifier, ClientType? clientType)
{
if (!string.IsNullOrWhiteSpace(identifier))
{
tag += $" && !deviceIdentifier:{SanitizeTagInput(identifier)}";
}

if (clientType.HasValue && clientType.Value != ClientType.All)
{
tag += $" && clientType:{clientType}";
}

return $"({tag})";
}

Expand All @@ -278,8 +308,7 @@ private async Task SendPayloadAsync(string tag, PushType type, object payload)
var results = await _notificationHubPool.AllClients.SendTemplateNotificationAsync(
new Dictionary<string, string>
{
{ "type", ((byte)type).ToString() },
{ "payload", JsonSerializer.Serialize(payload) }
{ "type", ((byte)type).ToString() }, { "payload", JsonSerializer.Serialize(payload) }
}, tag);

if (_enableTracing)
Expand All @@ -290,7 +319,9 @@ private async Task SendPayloadAsync(string tag, PushType type, object payload)
{
continue;
}
_logger.LogInformation("Azure Notification Hub Tracking ID: {Id} | {Type} push notification with {Success} successes and {Failure} failures with a payload of {@Payload} and result of {@Results}",

_logger.LogInformation(
"Azure Notification Hub Tracking ID: {Id} | {Type} push notification with {Success} successes and {Failure} failures with a payload of {@Payload} and result of {@Results}",
outcome.TrackingId, type, outcome.Success, outcome.Failure, payload, outcome.Results);
}
}
Expand Down
Loading
Loading