Greenfield: Add store id for notifications (#6093)

* Rename filter to storeid for consistency with other filters

* Greenfield: Add storeId to notification

* Cleanups

* Greenfield: Allow filtering notifications by store id
This commit is contained in:
d11n
2024-07-10 17:12:22 +02:00
committed by GitHub
parent d73e26e0c4
commit a4a1fa0746
10 changed files with 100 additions and 59 deletions

View File

@@ -9,7 +9,7 @@ namespace BTCPayServer.Client;
public partial class BTCPayServerClient
{
public virtual async Task<IEnumerable<NotificationData>> 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<string, object>();
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<IEnumerable<NotificationData>>("api/v1/users/me/notifications", queryPayload, HttpMethod.Get, token);
}

View File

@@ -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; }

View File

@@ -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<NotificationSender>()
.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<NotificationSender>()
.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<NotificationSender>()
.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);

View File

@@ -1,3 +1,4 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
@@ -36,65 +37,52 @@ 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<IActionResult> GetNotifications(bool? seen = null, [FromQuery] int? skip = null, [FromQuery] int? take = null)
public async Task<IActionResult> 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<IActionResult> 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 items.Count == 0 ? NotificationNotFound() : Ok(ToModel(items.Items.First()));
}
return 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<IActionResult> 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 items.Count == 0 ? NotificationNotFound() : Ok(ToModel(items.First()));
}
return 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<IActionResult> DeleteNotification(string id)
{
await _notificationManager.Remove(new NotificationsQuery()
await _notificationManager.Remove(new NotificationsQuery
{
Ids = new[] { id },
Ids = [id],
UserId = _userManager.GetUserId(User)
});
@@ -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");

View File

@@ -656,10 +656,10 @@ namespace BTCPayServer.Controllers.Greenfield
}
public override async Task<IEnumerable<NotificationData>> 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<IEnumerable<NotificationData>>(
await GetController<GreenfieldNotificationsController>().GetNotifications(seen, skip, take));
await GetController<GreenfieldNotificationsController>().GetNotifications(seen, skip, take, storeId));
}
public override async Task<NotificationData> GetNotification(string notificationId,

View File

@@ -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<ApplicationUser> _userManager;
private readonly NotificationManager _notificationManager;
@@ -27,13 +26,11 @@ namespace BTCPayServer.Controllers
public UINotificationsController(
StoreRepository storeRepo,
UserManager<ApplicationUser> 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<IActionResult> 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);
}

View File

@@ -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);

View File

@@ -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; }
}
}

View File

@@ -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 @@
<button id="StoresOptionsToggle" class="btn btn-secondary dropdown-toggle dropdown-toggle-custom-caret w-100 w-md-auto" type="button" data-bs-toggle="dropdown" aria-expanded="false">
@if (storesFilterCount > 0)
{
<span>@storesFilterCount Store</span>
<span>@storesFilterCount Store@(storesFilterCount > 1 ? "s" : "")</span>
}
else
{
@@ -99,8 +99,8 @@
<a asp-action="Index"
asp-route-count="@Model.Count"
asp-route-status="@Model.Status"
asp-route-searchTerm="@Model.Search.Toggle("store", store.Id)"
class="dropdown-item @(HasArrayFilter("store", store.Id) ? "custom-active" : "")">
asp-route-searchTerm="@Model.Search.Toggle("storeid", store.Id)"
class="dropdown-item @(HasArrayFilter("storeid", store.Id) ? "custom-active" : "")">
@store.StoreName
</a>
}

View File

@@ -36,6 +36,19 @@
"nullable": true,
"type": "number"
}
},
{
"name": "storeId",
"in": "query",
"required": false,
"description": "Array of store ids to fetch the notifications for",
"schema": {
"type": "array",
"items": {
"type": "string"
}
},
"example": "&storeId=ABCDE&storeId=FGHIJ"
}
],
"description": "View current user's notifications",
@@ -313,6 +326,11 @@
"format": "html",
"description": "The html body of the notifications"
},
"storeId": {
"type": "string",
"nullable": true,
"description": "If related to a store, the store id of the notification"
},
"link": {
"type": "string",
"format": "uri",