mirror of
https://github.com/bitwarden/server.git
synced 2024-12-01 13:43:23 +01:00
PM-10563: Notification Center API
This commit is contained in:
parent
b196c8bfb9
commit
ef32d33489
@ -0,0 +1,62 @@
|
|||||||
|
#nullable enable
|
||||||
|
using Bit.Api.Models.Response;
|
||||||
|
using Bit.Api.NotificationCenter.Models.Request;
|
||||||
|
using Bit.Api.NotificationCenter.Models.Response;
|
||||||
|
using Bit.Core.NotificationCenter.Commands.Interfaces;
|
||||||
|
using Bit.Core.NotificationCenter.Models.Filter;
|
||||||
|
using Bit.Core.NotificationCenter.Queries.Interfaces;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Bit.Api.NotificationCenter.Controllers;
|
||||||
|
|
||||||
|
[Route("notifications")]
|
||||||
|
[Authorize("Application")]
|
||||||
|
public class NotificationsController : Controller
|
||||||
|
{
|
||||||
|
private readonly IGetNotificationsForUserQuery _getNotificationsForUserQuery;
|
||||||
|
private readonly IMarkNotificationDeletedCommand _markNotificationDeletedCommand;
|
||||||
|
private readonly IMarkNotificationReadCommand _markNotificationReadCommand;
|
||||||
|
|
||||||
|
public NotificationsController(
|
||||||
|
IGetNotificationsForUserQuery getNotificationsForUserQuery,
|
||||||
|
IMarkNotificationDeletedCommand markNotificationDeletedCommand,
|
||||||
|
IMarkNotificationReadCommand markNotificationReadCommand)
|
||||||
|
{
|
||||||
|
_getNotificationsForUserQuery = getNotificationsForUserQuery;
|
||||||
|
_markNotificationDeletedCommand = markNotificationDeletedCommand;
|
||||||
|
_markNotificationReadCommand = markNotificationReadCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("")]
|
||||||
|
public async Task<ListResponseModel<NotificationResponseModel>> List(
|
||||||
|
[FromQuery] NotificationFilterRequestModel filter)
|
||||||
|
{
|
||||||
|
var notificationStatusFilter = new NotificationStatusFilter
|
||||||
|
{
|
||||||
|
Read = filter.ReadStatusFilter,
|
||||||
|
Deleted = filter.DeletedStatusFilter
|
||||||
|
};
|
||||||
|
|
||||||
|
var notifications = await _getNotificationsForUserQuery.GetByUserIdStatusFilterAsync(notificationStatusFilter);
|
||||||
|
|
||||||
|
var filteredNotifications = notifications
|
||||||
|
.Where(n => n.RevisionDate >= filter.Start && n.RevisionDate < filter.End)
|
||||||
|
.Take(filter.PageSize);
|
||||||
|
|
||||||
|
var responses = filteredNotifications.Select(n => new NotificationResponseModel(n));
|
||||||
|
return new ListResponseModel<NotificationResponseModel>(responses);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPatch("{id}/delete")]
|
||||||
|
public async Task MarkAsDeleted([FromRoute] Guid id)
|
||||||
|
{
|
||||||
|
await _markNotificationDeletedCommand.MarkDeletedAsync(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPatch("{id}/read")]
|
||||||
|
public async Task MarkAsRead([FromRoute] Guid id)
|
||||||
|
{
|
||||||
|
await _markNotificationReadCommand.MarkReadAsync(id);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
namespace Bit.Api.NotificationCenter.Models.Request;
|
||||||
|
|
||||||
|
public class NotificationFilterRequestModel
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Filters notifications by read status. When not set, includes notifications without a status.
|
||||||
|
/// </summary>
|
||||||
|
public bool? ReadStatusFilter { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Filters notifications by deleted status. When not set, includes notifications without a status.
|
||||||
|
/// </summary>
|
||||||
|
public bool? DeletedStatusFilter { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The start date. Must be less than the end date. Inclusive.
|
||||||
|
/// </summary>
|
||||||
|
public DateTime Start { get; set; } = DateTime.MinValue;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The end date. Must be greater than the start date. Not inclusive.
|
||||||
|
/// </summary>
|
||||||
|
public DateTime End { get; set; } = DateTime.MaxValue;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Number of items to return. Defaults to 10.
|
||||||
|
/// </summary>
|
||||||
|
public int PageSize { get; set; } = 10;
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
#nullable enable
|
||||||
|
using Bit.Core.Models.Api;
|
||||||
|
using Bit.Core.NotificationCenter.Entities;
|
||||||
|
using Bit.Core.NotificationCenter.Enums;
|
||||||
|
|
||||||
|
namespace Bit.Api.NotificationCenter.Models.Response;
|
||||||
|
|
||||||
|
public class NotificationResponseModel : ResponseModel
|
||||||
|
{
|
||||||
|
private const string _objectName = "notification";
|
||||||
|
|
||||||
|
public NotificationResponseModel(Notification notification, string obj = _objectName)
|
||||||
|
: base(obj)
|
||||||
|
{
|
||||||
|
if (notification == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(notification));
|
||||||
|
}
|
||||||
|
|
||||||
|
Id = notification.Id;
|
||||||
|
Priority = notification.Priority;
|
||||||
|
Title = notification.Title;
|
||||||
|
Body = notification.Body;
|
||||||
|
Date = notification.RevisionDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public NotificationResponseModel() : base(_objectName)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
|
public Priority Priority { get; set; }
|
||||||
|
|
||||||
|
public string? Title { get; set; }
|
||||||
|
|
||||||
|
public string? Body { get; set; }
|
||||||
|
|
||||||
|
public DateTime Date { get; set; }
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
using Bit.Core.NotificationCenter.Authorization;
|
||||||
|
using Bit.Core.NotificationCenter.Commands;
|
||||||
|
using Bit.Core.NotificationCenter.Commands.Interfaces;
|
||||||
|
using Bit.Core.NotificationCenter.Queries;
|
||||||
|
using Bit.Core.NotificationCenter.Queries.Interfaces;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Bit.Core.NotificationCenter;
|
||||||
|
|
||||||
|
public static class NotificationCenterServiceCollectionExtensions
|
||||||
|
{
|
||||||
|
public static void AddNotificationCenterServices(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
// Authorization Handlers
|
||||||
|
services.AddScoped<IAuthorizationHandler, NotificationAuthorizationHandler>();
|
||||||
|
services.AddScoped<IAuthorizationHandler, NotificationStatusAuthorizationHandler>();
|
||||||
|
// Commands
|
||||||
|
services.AddScoped<ICreateNotificationCommand, CreateNotificationCommand>();
|
||||||
|
services.AddScoped<ICreateNotificationStatusCommand, CreateNotificationStatusCommand>();
|
||||||
|
services.AddScoped<IMarkNotificationDeletedCommand, MarkNotificationDeletedCommand>();
|
||||||
|
services.AddScoped<IMarkNotificationReadCommand, MarkNotificationReadCommand>();
|
||||||
|
services.AddScoped<IUpdateNotificationCommand, UpdateNotificationCommand>();
|
||||||
|
// Queries
|
||||||
|
services.AddScoped<IGetNotificationsForUserQuery, GetNotificationsForUserQuery>();
|
||||||
|
services.AddScoped<IGetNotificationStatusForUserQuery, GetNotificationStatusForUserQuery>();
|
||||||
|
}
|
||||||
|
}
|
@ -24,6 +24,7 @@ using Bit.Core.Enums;
|
|||||||
using Bit.Core.HostedServices;
|
using Bit.Core.HostedServices;
|
||||||
using Bit.Core.Identity;
|
using Bit.Core.Identity;
|
||||||
using Bit.Core.IdentityServer;
|
using Bit.Core.IdentityServer;
|
||||||
|
using Bit.Core.NotificationCenter;
|
||||||
using Bit.Core.OrganizationFeatures;
|
using Bit.Core.OrganizationFeatures;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Resources;
|
using Bit.Core.Resources;
|
||||||
@ -114,6 +115,7 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddLoginServices();
|
services.AddLoginServices();
|
||||||
services.AddScoped<IOrganizationDomainService, OrganizationDomainService>();
|
services.AddScoped<IOrganizationDomainService, OrganizationDomainService>();
|
||||||
services.AddVaultServices();
|
services.AddVaultServices();
|
||||||
|
services.AddNotificationCenterServices();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void AddTokenizers(this IServiceCollection services)
|
public static void AddTokenizers(this IServiceCollection services)
|
||||||
|
@ -0,0 +1,52 @@
|
|||||||
|
#nullable enable
|
||||||
|
using Bit.Api.NotificationCenter.Controllers;
|
||||||
|
using Bit.Api.NotificationCenter.Models.Request;
|
||||||
|
using Bit.Core.NotificationCenter.Entities;
|
||||||
|
using Bit.Core.NotificationCenter.Models.Filter;
|
||||||
|
using Bit.Core.NotificationCenter.Queries.Interfaces;
|
||||||
|
using Bit.Core.Test.NotificationCenter.AutoFixture;
|
||||||
|
using Bit.Test.Common.AutoFixture;
|
||||||
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Api.Test.NotificationCenter.Controllers;
|
||||||
|
|
||||||
|
[ControllerCustomize(typeof(NotificationsController))]
|
||||||
|
[SutProviderCustomize]
|
||||||
|
public class NotificationsControllerTest
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
[NotificationListCustomize(20)]
|
||||||
|
public async Task List_DefaultFilter_ReturnedMatchingNotifications(SutProvider<NotificationsController> sutProvider,
|
||||||
|
List<Notification> notifications)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<IGetNotificationsForUserQuery>()
|
||||||
|
.GetByUserIdStatusFilterAsync(Arg.Any<NotificationStatusFilter>())
|
||||||
|
.Returns(notifications);
|
||||||
|
|
||||||
|
var expectedNotificationGroupedById = notifications
|
||||||
|
.Take(10)
|
||||||
|
.ToDictionary(n => n.Id);
|
||||||
|
|
||||||
|
var filter = new NotificationFilterRequestModel();
|
||||||
|
|
||||||
|
var listResponse = await sutProvider.Sut.List(filter);
|
||||||
|
|
||||||
|
Assert.Equal(10, listResponse.Data.Count());
|
||||||
|
Assert.All(listResponse.Data, notificationResponseModel =>
|
||||||
|
{
|
||||||
|
var expectedNotification = expectedNotificationGroupedById[notificationResponseModel.Id];
|
||||||
|
Assert.NotNull(expectedNotification);
|
||||||
|
Assert.Equal(expectedNotification.Id, notificationResponseModel.Id);
|
||||||
|
Assert.Equal(expectedNotification.Priority, notificationResponseModel.Priority);
|
||||||
|
Assert.Equal(expectedNotification.Title, notificationResponseModel.Title);
|
||||||
|
Assert.Equal(expectedNotification.Body, notificationResponseModel.Body);
|
||||||
|
Assert.Equal(expectedNotification.RevisionDate, notificationResponseModel.Date);
|
||||||
|
Assert.Equal("notification", notificationResponseModel.Object);
|
||||||
|
});
|
||||||
|
Assert.Null(listResponse.ContinuationToken);
|
||||||
|
Assert.Equal("list", listResponse.Object);
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,6 @@
|
|||||||
using AutoFixture;
|
using AutoFixture;
|
||||||
|
using AutoFixture.Dsl;
|
||||||
|
using AutoFixture.Kernel;
|
||||||
using Bit.Core.NotificationCenter.Entities;
|
using Bit.Core.NotificationCenter.Entities;
|
||||||
using Bit.Test.Common.AutoFixture.Attributes;
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
|
||||||
@ -8,9 +10,12 @@ public class NotificationCustomization(bool global) : ICustomization
|
|||||||
{
|
{
|
||||||
public void Customize(IFixture fixture)
|
public void Customize(IFixture fixture)
|
||||||
{
|
{
|
||||||
fixture.Customize<Notification>(composer =>
|
fixture.Customize<Notification>(GetSpecimenBuilder);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ISpecimenBuilder GetSpecimenBuilder(ICustomizationComposer<Notification> customizationComposer)
|
||||||
{
|
{
|
||||||
var postprocessComposer = composer.With(n => n.Id, Guid.NewGuid())
|
var postprocessComposer = customizationComposer.With(n => n.Id, Guid.NewGuid())
|
||||||
.With(n => n.Global, global);
|
.With(n => n.Global, global);
|
||||||
|
|
||||||
postprocessComposer = global
|
postprocessComposer = global
|
||||||
@ -20,7 +25,29 @@ public class NotificationCustomization(bool global) : ICustomization
|
|||||||
return global
|
return global
|
||||||
? postprocessComposer.Without(n => n.OrganizationId)
|
? postprocessComposer.Without(n => n.OrganizationId)
|
||||||
: postprocessComposer.With(n => n.OrganizationId, Guid.NewGuid());
|
: postprocessComposer.With(n => n.OrganizationId, Guid.NewGuid());
|
||||||
});
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class NotificationListCustomization(int count) : ICustomization
|
||||||
|
{
|
||||||
|
public void Customize(IFixture fixture)
|
||||||
|
{
|
||||||
|
fixture.Customize<List<Notification>>(composer => composer.FromFactory(() =>
|
||||||
|
{
|
||||||
|
var notificationCustomization = new NotificationCustomization(true);
|
||||||
|
|
||||||
|
var notifications = new List<Notification>();
|
||||||
|
for (var i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
var customizationComposer = fixture.Build<Notification>();
|
||||||
|
var postprocessComposer =
|
||||||
|
customizationComposer.FromFactory(
|
||||||
|
notificationCustomization.GetSpecimenBuilder(customizationComposer));
|
||||||
|
notifications.Add(postprocessComposer.Create());
|
||||||
|
}
|
||||||
|
|
||||||
|
return notifications;
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -29,3 +56,9 @@ public class NotificationCustomizeAttribute(bool global = true)
|
|||||||
{
|
{
|
||||||
public override ICustomization GetCustomization() => new NotificationCustomization(global);
|
public override ICustomization GetCustomization() => new NotificationCustomization(global);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class NotificationListCustomizeAttribute(int count)
|
||||||
|
: BitCustomizeAttribute
|
||||||
|
{
|
||||||
|
public override ICustomization GetCustomization() => new NotificationListCustomization(count);
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user