diff --git a/BTCPayServer.Client/BTCPayServerClient.Notifications.cs b/BTCPayServer.Client/BTCPayServerClient.Notifications.cs index bdae85d30..5c55f9e37 100644 --- a/BTCPayServer.Client/BTCPayServerClient.Notifications.cs +++ b/BTCPayServer.Client/BTCPayServerClient.Notifications.cs @@ -9,7 +9,7 @@ namespace BTCPayServer.Client; public partial class BTCPayServerClient { public virtual async Task> GetNotifications(bool? seen = null, int? skip = null, - int? take = null, CancellationToken token = default) + int? take = null, string[] storeId = null, CancellationToken token = default) { var queryPayload = new Dictionary(); if (seen != null) @@ -18,6 +18,8 @@ public partial class BTCPayServerClient queryPayload.Add(nameof(skip), skip); if (take != null) queryPayload.Add(nameof(take), take); + if (storeId != null) + queryPayload.Add(nameof(storeId), storeId); return await SendHttpRequest>("api/v1/users/me/notifications", queryPayload, HttpMethod.Get, token); } diff --git a/BTCPayServer.Client/Models/NotificationData.cs b/BTCPayServer.Client/Models/NotificationData.cs index ef1014738..b0306f136 100644 --- a/BTCPayServer.Client/Models/NotificationData.cs +++ b/BTCPayServer.Client/Models/NotificationData.cs @@ -9,6 +9,7 @@ namespace BTCPayServer.Client.Models public string Identifier { get; set; } public string Type { get; set; } public string Body { get; set; } + public string StoreId { get; set; } public bool Seen { get; set; } public Uri Link { get; set; } diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index f61b1cdc4..00439a4ad 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; -using System.Net.Http; using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; @@ -13,7 +13,6 @@ using BTCPayServer.Events; using BTCPayServer.Lightning; using BTCPayServer.Models.InvoicingModels; using BTCPayServer.NTag424; -using BTCPayServer.Payments; using BTCPayServer.Payments.Lightning; using BTCPayServer.PayoutProcessors; using BTCPayServer.Services; @@ -30,7 +29,6 @@ using Newtonsoft.Json.Linq; using Xunit; using Xunit.Abstractions; using Xunit.Sdk; -using static Org.BouncyCastle.Math.EC.ECCurve; using CreateApplicationUserRequest = BTCPayServer.Client.Models.CreateApplicationUserRequest; namespace BTCPayServer.Tests @@ -2915,14 +2913,18 @@ namespace BTCPayServer.Tests await tester.PayTester.GetService() .SendNotification(new UserScope(user.UserId), new NewVersionNotification()); - Assert.Single(await viewOnlyClient.GetNotifications()); + var notifications = (await viewOnlyClient.GetNotifications()).ToList(); + Assert.Single(notifications); Assert.Single(await viewOnlyClient.GetNotifications(false)); Assert.Empty(await viewOnlyClient.GetNotifications(true)); + var notification = notifications.First(); + Assert.Null(notification.StoreId); + Assert.Single(await client.GetNotifications()); Assert.Single(await client.GetNotifications(false)); Assert.Empty(await client.GetNotifications(true)); - var notification = (await client.GetNotifications()).First(); + notification = (await client.GetNotifications()).First(); notification = await client.GetNotification(notification.Id); Assert.False(notification.Seen); await AssertHttpError(403, async () => @@ -2940,6 +2942,41 @@ namespace BTCPayServer.Tests Assert.Empty(await viewOnlyClient.GetNotifications(true)); Assert.Empty(await viewOnlyClient.GetNotifications(false)); + // Store association + var unrestricted = await user.CreateClient(Policies.Unrestricted); + var store1 = await unrestricted.CreateStore(new CreateStoreRequest { Name = "Store A" }); + await tester.PayTester.GetService() + .SendNotification(new UserScope(user.UserId), new InviteAcceptedNotification{ + UserId = user.UserId, + UserEmail = user.Email, + StoreId = store1.Id, + StoreName = store1.Name + }); + notifications = (await client.GetNotifications()).ToList(); + Assert.Single(notifications); + + notification = notifications.First(); + Assert.Equal(store1.Id, notification.StoreId); + Assert.Equal($"User {user.Email} accepted the invite to {store1.Name}.", notification.Body); + + var store2 = await unrestricted.CreateStore(new CreateStoreRequest { Name = "Store B" }); + await tester.PayTester.GetService() + .SendNotification(new UserScope(user.UserId), new InviteAcceptedNotification{ + UserId = user.UserId, + UserEmail = user.Email, + StoreId = store2.Id, + StoreName = store2.Name + }); + notifications = (await client.GetNotifications(storeId: [store2.Id])).ToList(); + Assert.Single(notifications); + + notification = notifications.First(); + Assert.Equal(store2.Id, notification.StoreId); + Assert.Equal($"User {user.Email} accepted the invite to {store2.Name}.", notification.Body); + + Assert.Equal(2, (await client.GetNotifications(storeId: [store1.Id, store2.Id])).Count()); + Assert.Equal(2, (await client.GetNotifications()).Count()); + // Settings var settings = await client.GetNotificationSettings(); Assert.True(settings.Notifications.Find(n => n.Identifier == "newversion").Enabled); diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldNotificationsController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldNotificationsController.cs index 2d86d7c22..4e3a7c559 100644 --- a/BTCPayServer/Controllers/GreenField/GreenfieldNotificationsController.cs +++ b/BTCPayServer/Controllers/GreenField/GreenfieldNotificationsController.cs @@ -1,3 +1,4 @@ +#nullable enable using System; using System.Collections.Generic; using System.Linq; @@ -36,71 +37,58 @@ namespace BTCPayServer.Controllers.Greenfield _notificationHandlers = notificationHandlers; } - [Authorize(Policy = Policies.CanViewNotificationsForUser, - AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + [Authorize(Policy = Policies.CanViewNotificationsForUser, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] [HttpGet("~/api/v1/users/me/notifications")] - public async Task GetNotifications(bool? seen = null, [FromQuery] int? skip = null, [FromQuery] int? take = null) + public async Task GetNotifications(bool? seen = null, [FromQuery] int? skip = null, [FromQuery] int? take = null, [FromQuery] string[]? storeId = null) { - var items = await _notificationManager.GetNotifications(new NotificationsQuery() + var items = await _notificationManager.GetNotifications(new NotificationsQuery { Seen = seen, UserId = _userManager.GetUserId(User), Skip = skip, - Take = take + Take = take, + StoreIds = storeId, }); return Ok(items.Items.Select(ToModel)); } - [Authorize(Policy = Policies.CanViewNotificationsForUser, - AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + [Authorize(Policy = Policies.CanViewNotificationsForUser, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] [HttpGet("~/api/v1/users/me/notifications/{id}")] public async Task GetNotification(string id) { - var items = await _notificationManager.GetNotifications(new NotificationsQuery() + var items = await _notificationManager.GetNotifications(new NotificationsQuery { - Ids = new[] { id }, + Ids = [id], UserId = _userManager.GetUserId(User) }); - if (items.Count == 0) - { - return NotificationNotFound(); - } - - return Ok(ToModel(items.Items.First())); + return items.Count == 0 ? NotificationNotFound() : Ok(ToModel(items.Items.First())); } - [Authorize(Policy = Policies.CanManageNotificationsForUser, - AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + [Authorize(Policy = Policies.CanManageNotificationsForUser, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] [HttpPut("~/api/v1/users/me/notifications/{id}")] public async Task UpdateNotification(string id, UpdateNotification request) { var items = await _notificationManager.ToggleSeen( - new NotificationsQuery() { Ids = new[] { id }, UserId = _userManager.GetUserId(User) }, request.Seen); + new NotificationsQuery { Ids = [id], UserId = _userManager.GetUserId(User) }, request.Seen); - if (items.Count == 0) - { - return NotificationNotFound(); - } - - return Ok(ToModel(items.First())); + return items.Count == 0 ? NotificationNotFound() : Ok(ToModel(items.First())); } - [Authorize(Policy = Policies.CanManageNotificationsForUser, - AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + [Authorize(Policy = Policies.CanManageNotificationsForUser, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] [HttpDelete("~/api/v1/users/me/notifications/{id}")] public async Task DeleteNotification(string id) { - await _notificationManager.Remove(new NotificationsQuery() + await _notificationManager.Remove(new NotificationsQuery { - Ids = new[] { id }, + Ids = [id], UserId = _userManager.GetUserId(User) }); return Ok(); } - + [Authorize(Policy = Policies.CanManageNotificationsForUser, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] [HttpGet("~/api/v1/users/me/notification-settings")] public async Task GetNotificationSettings() @@ -132,7 +120,7 @@ namespace BTCPayServer.Controllers.Greenfield return Ok(model); } - private NotificationData ToModel(NotificationViewModel entity) + private static NotificationData ToModel(NotificationViewModel entity) { return new NotificationData { @@ -141,10 +129,12 @@ namespace BTCPayServer.Controllers.Greenfield Type = entity.Type, CreatedTime = entity.Created, Body = entity.Body, + StoreId = entity.StoreId, Seen = entity.Seen, Link = string.IsNullOrEmpty(entity.ActionLink) ? null : new Uri(entity.ActionLink) }; } + private IActionResult NotificationNotFound() { return this.CreateAPIError(404, "notification-not-found", "The notification was not found"); diff --git a/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs b/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs index 2d443d3dc..338230a14 100644 --- a/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs +++ b/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs @@ -656,10 +656,10 @@ namespace BTCPayServer.Controllers.Greenfield } public override async Task> GetNotifications(bool? seen = null, - int? skip = null, int? take = null, CancellationToken token = default) + int? skip = null, int? take = null, string[] storeId = null, CancellationToken token = default) { return GetFromActionResult>( - await GetController().GetNotifications(seen, skip, take)); + await GetController().GetNotifications(seen, skip, take, storeId)); } public override async Task GetNotification(string notificationId, diff --git a/BTCPayServer/Controllers/UINotificationsController.cs b/BTCPayServer/Controllers/UINotificationsController.cs index 02f83e18e..339e2ef82 100644 --- a/BTCPayServer/Controllers/UINotificationsController.cs +++ b/BTCPayServer/Controllers/UINotificationsController.cs @@ -19,7 +19,6 @@ namespace BTCPayServer.Controllers [Route("notifications/{action:lowercase=Index}")] public class UINotificationsController : Controller { - private readonly ApplicationDbContextFactory _factory; private readonly StoreRepository _storeRepo; private readonly UserManager _userManager; private readonly NotificationManager _notificationManager; @@ -27,13 +26,11 @@ namespace BTCPayServer.Controllers public UINotificationsController( StoreRepository storeRepo, UserManager userManager, - NotificationManager notificationManager, - ApplicationDbContextFactory factory) + NotificationManager notificationManager) { _storeRepo = storeRepo; _userManager = userManager; _notificationManager = notificationManager; - _factory = factory; } [HttpGet] @@ -49,9 +46,6 @@ namespace BTCPayServer.Controllers var stores = await _storeRepo.GetStoresByUserId(userId); model.Stores = stores.Where(store => !store.Archived).OrderBy(s => s.StoreName).ToList(); - - await using var dbContext = _factory.CreateContext(); - var searchTerm = string.IsNullOrEmpty(model.SearchText) ? model.SearchTerm : $"{model.SearchText},{model.SearchTerm}"; var fs = new SearchString(searchTerm, timezoneOffset); model.Search = fs; @@ -63,7 +57,7 @@ namespace BTCPayServer.Controllers UserId = userId, SearchText = model.SearchText, Type = fs.GetFilterArray("type"), - Stores = fs.GetFilterArray("store"), + StoreIds = fs.GetFilterArray("storeid"), Seen = model.Status == "Unread" ? false : null }); model.Items = res.Items; @@ -71,14 +65,13 @@ namespace BTCPayServer.Controllers return View(model); } - [HttpPost] [Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanManageNotificationsForUser)] public async Task FlipRead(string id) { if (ValidUserClaim(out var userId)) { - await _notificationManager.ToggleSeen(new NotificationsQuery() { Ids = new[] { id }, UserId = userId }, null); + await _notificationManager.ToggleSeen(new NotificationsQuery { Ids = [id], UserId = userId }, null); return RedirectToAction(nameof(Index)); } @@ -91,9 +84,9 @@ namespace BTCPayServer.Controllers if (ValidUserClaim(out var userId)) { var items = await - _notificationManager.ToggleSeen(new NotificationsQuery() + _notificationManager.ToggleSeen(new NotificationsQuery { - Ids = new[] { id }, + Ids = [id], UserId = userId }, true); @@ -168,7 +161,7 @@ namespace BTCPayServer.Controllers { return NotFound(); } - await _notificationManager.ToggleSeen(new NotificationsQuery() { Seen = false, UserId = userId }, true); + await _notificationManager.ToggleSeen(new NotificationsQuery { Seen = false, UserId = userId }, true); return LocalRedirect(returnUrl); } diff --git a/BTCPayServer/Services/Notifications/Blobs/InvoiceEventNotification.cs b/BTCPayServer/Services/Notifications/Blobs/InvoiceEventNotification.cs index ab477ed2a..b3f61c7a7 100644 --- a/BTCPayServer/Services/Notifications/Blobs/InvoiceEventNotification.cs +++ b/BTCPayServer/Services/Notifications/Blobs/InvoiceEventNotification.cs @@ -54,7 +54,7 @@ namespace BTCPayServer.Services.Notifications.Blobs } vm.Identifier = notification.Identifier; vm.Type = notification.NotificationType; - vm.StoreId = notification?.StoreId; + vm.StoreId = notification.StoreId; vm.ActionLink = _linkGenerator.GetPathByAction(nameof(UIInvoiceController.Invoice), "UIInvoice", new { invoiceId = notification.InvoiceId }, _options.RootPath); diff --git a/BTCPayServer/Services/Notifications/NotificationManager.cs b/BTCPayServer/Services/Notifications/NotificationManager.cs index 0b5b62ab8..6ad8e0052 100644 --- a/BTCPayServer/Services/Notifications/NotificationManager.cs +++ b/BTCPayServer/Services/Notifications/NotificationManager.cs @@ -151,9 +151,9 @@ namespace BTCPayServer.Services.Notifications } } } - if (query.Stores?.Length > 0) + if (query.StoreIds?.Length > 0) { - notifications = notifications.Where(n => !string.IsNullOrEmpty(n.StoreId) && query.Stores.Contains(n.StoreId, StringComparer.OrdinalIgnoreCase)).ToList(); + notifications = notifications.Where(n => !string.IsNullOrEmpty(n.StoreId) && query.StoreIds.Contains(n.StoreId, StringComparer.OrdinalIgnoreCase)).ToList(); } return notifications; } @@ -221,6 +221,6 @@ namespace BTCPayServer.Services.Notifications public bool? Seen { get; set; } public string SearchText { get; set; } public string[] Type { get; set; } - public string[] Stores { get; set; } + public string[] StoreIds { get; set; } } } diff --git a/BTCPayServer/Views/UINotifications/Index.cshtml b/BTCPayServer/Views/UINotifications/Index.cshtml index a376b9e4a..eb41839f0 100644 --- a/BTCPayServer/Views/UINotifications/Index.cshtml +++ b/BTCPayServer/Views/UINotifications/Index.cshtml @@ -3,7 +3,7 @@ ViewData["Title"] = "Notifications"; string status = ViewBag.Status; var statusFilterCount = CountArrayFilter("type"); - var storesFilterCount = CountArrayFilter("store"); + var storesFilterCount = CountArrayFilter("storeid"); } @functions @@ -86,7 +86,7 @@