mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-17 05:54:26 +01:00
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:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<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 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<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 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<IActionResult> 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<IActionResult> 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");
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user