mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2024-11-19 01:43:50 +01:00
GreenField: Notifications API (#2055)
* GreenField: Notifications API This refactors notifications so that we dont have a bunch of duplicated direct access to db contexts in controllers and then introduces new endpoints to fetch/toggle seen/remove notifications of the current user. * add tests + docs * fix test * pr changes * fix permission json
This commit is contained in:
parent
1c58fabc30
commit
0652e30c30
47
BTCPayServer.Client/BTCPayServerClient.Notifications.cs
Normal file
47
BTCPayServer.Client/BTCPayServerClient.Notifications.cs
Normal file
@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
|
||||
namespace BTCPayServer.Client
|
||||
{
|
||||
public partial class BTCPayServerClient
|
||||
{
|
||||
public virtual async Task<IEnumerable<NotificationData>> GetNotifications(bool? seen = null,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var response =
|
||||
await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/users/me/notifications",
|
||||
seen != null ? new Dictionary<string, object>() {{nameof(seen), seen}} : null), token);
|
||||
return await HandleResponse<IEnumerable<NotificationData>>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<NotificationData> GetNotification(string notificationId,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/users/me/notifications/{notificationId}"), token);
|
||||
return await HandleResponse<NotificationData>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<NotificationData> UpdateNotification(string notificationId, bool? seen,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/users/me/notifications/{notificationId}",
|
||||
method: HttpMethod.Put, bodyPayload: new UpdateNotification() {Seen = seen}), token);
|
||||
return await HandleResponse<NotificationData>(response);
|
||||
}
|
||||
|
||||
public virtual async Task RemoveNotification(string notificationId, CancellationToken token = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/users/me/notifications/{notificationId}",
|
||||
method: HttpMethod.Delete), token);
|
||||
await HandleResponse(response);
|
||||
}
|
||||
}
|
||||
}
|
16
BTCPayServer.Client/Models/NotificationData.cs
Normal file
16
BTCPayServer.Client/Models/NotificationData.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class NotificationData
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Body { get; set; }
|
||||
public bool Seen { get; set; }
|
||||
public Uri Link { get; set; }
|
||||
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||
public DateTimeOffset CreatedTime { get; set; }
|
||||
}
|
||||
}
|
7
BTCPayServer.Client/Models/UpdateNotification.cs
Normal file
7
BTCPayServer.Client/Models/UpdateNotification.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class UpdateNotification
|
||||
{
|
||||
public bool? Seen { get; set; }
|
||||
}
|
||||
}
|
@ -21,6 +21,8 @@ namespace BTCPayServer.Client
|
||||
public const string CanModifyPaymentRequests = "btcpay.store.canmodifypaymentrequests";
|
||||
public const string CanModifyProfile = "btcpay.user.canmodifyprofile";
|
||||
public const string CanViewProfile = "btcpay.user.canviewprofile";
|
||||
public const string CanManageNotificationsForUser = "btcpay.user.canmanagenotificationsforuser";
|
||||
public const string CanViewNotificationsForUser = "btcpay.user.canviewnotificationsforuser";
|
||||
public const string CanCreateUser = "btcpay.server.cancreateuser";
|
||||
public const string CanManagePullPayments = "btcpay.store.canmanagepullpayments";
|
||||
public const string Unrestricted = "unrestricted";
|
||||
@ -39,6 +41,8 @@ namespace BTCPayServer.Client
|
||||
yield return CanModifyProfile;
|
||||
yield return CanViewProfile;
|
||||
yield return CanCreateUser;
|
||||
yield return CanManageNotificationsForUser;
|
||||
yield return CanViewNotificationsForUser;
|
||||
yield return Unrestricted;
|
||||
yield return CanUseInternalLightningNode;
|
||||
yield return CanCreateLightningInvoiceInternalNode;
|
||||
@ -168,6 +172,7 @@ namespace BTCPayServer.Client
|
||||
case Policies.CanViewPaymentRequests when this.Policy == Policies.CanViewStoreSettings:
|
||||
case Policies.CanCreateLightningInvoiceInternalNode when this.Policy == Policies.CanUseInternalLightningNode:
|
||||
case Policies.CanCreateLightningInvoiceInStore when this.Policy == Policies.CanUseLightningNodeInStore:
|
||||
case Policies.CanViewNotificationsForUser when this.Policy == Policies.CanManageNotificationsForUser:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
|
@ -12,7 +12,8 @@ using BTCPayServer.JsonConverters;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Models.InvoicingModels;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Notifications;
|
||||
using BTCPayServer.Services.Notifications.Blobs;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@ -21,7 +22,6 @@ using NBitcoin.OpenAsset;
|
||||
using NBitpayClient;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NUglify.Helpers;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using CreateApplicationUserRequest = BTCPayServer.Client.Models.CreateApplicationUserRequest;
|
||||
@ -1156,6 +1156,46 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task NotificationAPITests()
|
||||
{
|
||||
using var tester = ServerTester.Create();
|
||||
await tester.StartAsync();
|
||||
var user = tester.NewAccount();
|
||||
await user.GrantAccessAsync(true);
|
||||
var client = await user.CreateClient(Policies.CanManageNotificationsForUser);
|
||||
var viewOnlyClient = await user.CreateClient(Policies.CanViewNotificationsForUser);
|
||||
await tester.PayTester.GetService<NotificationSender>()
|
||||
.SendNotification(new UserScope(user.UserId), new NewVersionNotification());
|
||||
|
||||
Assert.Single(await viewOnlyClient.GetNotifications());
|
||||
Assert.Single(await viewOnlyClient.GetNotifications(false));
|
||||
Assert.Empty(await viewOnlyClient.GetNotifications(true));
|
||||
|
||||
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.GetNotification(notification.Id);
|
||||
Assert.False(notification.Seen);
|
||||
await AssertHttpError(403, async () =>
|
||||
{
|
||||
await viewOnlyClient.UpdateNotification(notification.Id, true);
|
||||
});
|
||||
await AssertHttpError(403, async () =>
|
||||
{
|
||||
await viewOnlyClient.RemoveNotification(notification.Id);
|
||||
});
|
||||
Assert.True((await client.UpdateNotification(notification.Id, true)).Seen);
|
||||
Assert.Single(await viewOnlyClient.GetNotifications(true));
|
||||
Assert.Empty(await viewOnlyClient.GetNotifications(false));
|
||||
await client.RemoveNotification(notification.Id);
|
||||
Assert.Empty(await viewOnlyClient.GetNotifications(true));
|
||||
Assert.Empty(await viewOnlyClient.GetNotifications(false));
|
||||
}
|
||||
|
||||
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Fast", "Fast")]
|
||||
@ -1197,5 +1237,6 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(1.2, jsonConverter.ReadJson(Get(stringJson), typeof(double), null, null));
|
||||
Assert.Equal(1.2, jsonConverter.ReadJson(Get(stringJson), typeof(double?), null, null));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -1322,7 +1322,7 @@ namespace BTCPayServer.Tests
|
||||
var resp = await ctrl.Generate(newVersion);
|
||||
|
||||
var vm = Assert.IsType<Models.NotificationViewModels.IndexViewModel>(
|
||||
Assert.IsType<ViewResult>(ctrl.Index()).Model);
|
||||
Assert.IsType<ViewResult>(await ctrl.Index()).Model);
|
||||
|
||||
Assert.True(vm.Skip == 0);
|
||||
Assert.True(vm.Count == 50);
|
||||
@ -3330,7 +3330,7 @@ namespace BTCPayServer.Tests
|
||||
var newVersion = MockVersionFetcher.MOCK_NEW_VERSION;
|
||||
|
||||
var vm = Assert.IsType<Models.NotificationViewModels.IndexViewModel>(
|
||||
Assert.IsType<ViewResult>(ctrl.Index()).Model);
|
||||
Assert.IsType<ViewResult>(await ctrl.Index()).Model);
|
||||
|
||||
Assert.True(vm.Skip == 0);
|
||||
Assert.True(vm.Count == 50);
|
||||
|
105
BTCPayServer/Controllers/GreenField/NotificationsController.cs
Normal file
105
BTCPayServer/Controllers/GreenField/NotificationsController.cs
Normal file
@ -0,0 +1,105 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Notifications;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NotificationData = BTCPayServer.Client.Models.NotificationData;
|
||||
|
||||
namespace BTCPayServer.Controllers.GreenField
|
||||
{
|
||||
[ApiController]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public class NotificationsController : ControllerBase
|
||||
{
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly NotificationManager _notificationManager;
|
||||
|
||||
public NotificationsController(UserManager<ApplicationUser> userManager,
|
||||
NotificationManager notificationManager)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_notificationManager = notificationManager;
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanViewNotificationsForUser,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/users/me/notifications")]
|
||||
public async Task<IActionResult> GetNotifications(bool? seen = null)
|
||||
{
|
||||
var items = await _notificationManager.GetNotifications(new NotificationsQuery()
|
||||
{
|
||||
Seen = seen, UserId = _userManager.GetUserId(User)
|
||||
});
|
||||
|
||||
return Ok(items.Items.Select(ToModel));
|
||||
}
|
||||
|
||||
[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()
|
||||
{
|
||||
Ids = new[] {id}, UserId = _userManager.GetUserId(User)
|
||||
});
|
||||
|
||||
if (items.Count == 0)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return Ok(ToModel(items.Items.First()));
|
||||
}
|
||||
|
||||
[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);
|
||||
|
||||
if (items.Count == 0)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return Ok(ToModel(items.First()));
|
||||
}
|
||||
|
||||
[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()
|
||||
{
|
||||
Ids = new[] {id}, UserId = _userManager.GetUserId(User)
|
||||
});
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
private NotificationData ToModel(NotificationViewModel entity)
|
||||
{
|
||||
return new NotificationData()
|
||||
{
|
||||
Id = entity.Id,
|
||||
CreatedTime = entity.Created,
|
||||
Body = entity.Body,
|
||||
Seen = entity.Seen,
|
||||
Link = string.IsNullOrEmpty(entity.ActionLink) ? null : new Uri(entity.ActionLink)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -18,12 +18,12 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
[ApiController]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public class GreenFieldController : ControllerBase
|
||||
public class GreenFieldStoresController : ControllerBase
|
||||
{
|
||||
private readonly StoreRepository _storeRepository;
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
|
||||
public GreenFieldController(StoreRepository storeRepository, UserManager<ApplicationUser> userManager)
|
||||
public GreenFieldStoresController(StoreRepository storeRepository, UserManager<ApplicationUser> userManager)
|
||||
{
|
||||
_storeRepository = storeRepository;
|
||||
_userManager = userManager;
|
||||
|
@ -17,7 +17,6 @@ using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NicolasDorier.RateLimits;
|
||||
|
||||
namespace BTCPayServer.Controllers.GreenField
|
||||
@ -28,7 +27,6 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
public class UsersController : ControllerBase
|
||||
{
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly BTCPayServerOptions _btcPayServerOptions;
|
||||
private readonly RoleManager<IdentityRole> _roleManager;
|
||||
private readonly SettingsRepository _settingsRepository;
|
||||
private readonly EventAggregator _eventAggregator;
|
||||
@ -38,8 +36,9 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly CssThemeManager _themeManager;
|
||||
|
||||
public UsersController(UserManager<ApplicationUser> userManager, BTCPayServerOptions btcPayServerOptions,
|
||||
RoleManager<IdentityRole> roleManager, SettingsRepository settingsRepository,
|
||||
public UsersController(UserManager<ApplicationUser> userManager,
|
||||
RoleManager<IdentityRole> roleManager,
|
||||
SettingsRepository settingsRepository,
|
||||
EventAggregator eventAggregator,
|
||||
IPasswordValidator<ApplicationUser> passwordValidator,
|
||||
RateLimitService throttleService,
|
||||
@ -48,7 +47,6 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
CssThemeManager themeManager)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_btcPayServerOptions = btcPayServerOptions;
|
||||
_roleManager = roleManager;
|
||||
_settingsRepository = settingsRepository;
|
||||
_eventAggregator = eventAggregator;
|
||||
|
@ -474,6 +474,8 @@ namespace BTCPayServer.Controllers
|
||||
{BTCPayServer.Client.Policies.CanModifyServerSettings, ("Manage your server", "The app will have total control on the server settings of your server")},
|
||||
{BTCPayServer.Client.Policies.CanViewProfile, ("View your profile", "The app will be able to view your user profile.")},
|
||||
{BTCPayServer.Client.Policies.CanModifyProfile, ("Manage your profile", "The app will be able to view and modify your user profile.")},
|
||||
{BTCPayServer.Client.Policies.CanManageNotificationsForUser, ("Manage your notifications", "The app will be able to view and modify your user notifications.")},
|
||||
{BTCPayServer.Client.Policies.CanViewNotificationsForUser, ("View your notifications", "The app will be able to view your user notifications.")},
|
||||
{BTCPayServer.Client.Policies.CanCreateInvoice, ("Create an invoice", "The app will be able to create new invoices.")},
|
||||
{$"{BTCPayServer.Client.Policies.CanCreateInvoice}:", ("Create an invoice", "The app will be able to create new invoices on the selected stores.")},
|
||||
{BTCPayServer.Client.Policies.CanViewInvoices, ("View invoices", "The app will be able to view invoices.")},
|
||||
|
@ -22,21 +22,18 @@ namespace BTCPayServer.Controllers
|
||||
public class NotificationsController : Controller
|
||||
{
|
||||
private readonly BTCPayServerEnvironment _env;
|
||||
private readonly ApplicationDbContext _db;
|
||||
private readonly NotificationSender _notificationSender;
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly NotificationManager _notificationManager;
|
||||
private readonly EventAggregator _eventAggregator;
|
||||
|
||||
public NotificationsController(BTCPayServerEnvironment env,
|
||||
ApplicationDbContext db,
|
||||
NotificationSender notificationSender,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
NotificationManager notificationManager,
|
||||
EventAggregator eventAggregator)
|
||||
{
|
||||
_env = env;
|
||||
_db = db;
|
||||
_notificationSender = notificationSender;
|
||||
_userManager = userManager;
|
||||
_notificationManager = notificationManager;
|
||||
@ -57,6 +54,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
var websocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
|
||||
var userId = _userManager.GetUserId(User);
|
||||
var websocketHelper = new WebSocketHelper(websocket);
|
||||
@ -90,34 +88,30 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
#if DEBUG
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GenerateJunk(int x = 100, bool admin=true)
|
||||
public async Task<IActionResult> GenerateJunk(int x = 100, bool admin = true)
|
||||
{
|
||||
for (int i = 0; i < x; i++)
|
||||
{
|
||||
await _notificationSender.SendNotification(admin? (NotificationScope) new AdminScope(): new UserScope(_userManager.GetUserId(User)), new JunkNotification());
|
||||
await _notificationSender.SendNotification(
|
||||
admin ? (NotificationScope)new AdminScope() : new UserScope(_userManager.GetUserId(User)),
|
||||
new JunkNotification());
|
||||
}
|
||||
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
#endif
|
||||
[HttpGet]
|
||||
public IActionResult Index(int skip = 0, int count = 50, int timezoneOffset = 0)
|
||||
public async Task<IActionResult> Index(int skip = 0, int count = 50, int timezoneOffset = 0)
|
||||
{
|
||||
if (!ValidUserClaim(out var userId))
|
||||
return RedirectToAction("Index", "Home");
|
||||
|
||||
var model = new IndexViewModel()
|
||||
var res = await _notificationManager.GetNotifications(new NotificationsQuery()
|
||||
{
|
||||
Skip = skip,
|
||||
Count = count,
|
||||
Items = _db.Notifications
|
||||
.Where(a => a.ApplicationUserId == userId)
|
||||
.OrderByDescending(a => a.Created)
|
||||
.Skip(skip).Take(count)
|
||||
.Select(a => _notificationManager.ToViewModel(a))
|
||||
.ToList(),
|
||||
Total = _db.Notifications.Count(a => a.ApplicationUserId == userId)
|
||||
};
|
||||
Skip = skip, Take = count, UserId = userId
|
||||
});
|
||||
|
||||
var model = new IndexViewModel() {Skip = skip, Count = count, Items = res.Items, Total = res.Count};
|
||||
|
||||
return View(model);
|
||||
}
|
||||
@ -136,10 +130,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
if (ValidUserClaim(out var userId))
|
||||
{
|
||||
var notif = _db.Notifications.Single(a => a.Id == id && a.ApplicationUserId == userId);
|
||||
notif.Seen = !notif.Seen;
|
||||
await _db.SaveChangesAsync();
|
||||
_notificationManager.InvalidateNotificationCache(userId);
|
||||
await _notificationManager.ToggleSeen(new NotificationsQuery() {Ids = new[] {id}, UserId = userId}, null);
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
@ -151,21 +142,19 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
if (ValidUserClaim(out var userId))
|
||||
{
|
||||
var notif = _db.Notifications.Single(a => a.Id == id && a.ApplicationUserId == userId);
|
||||
if (!notif.Seen)
|
||||
var items = await
|
||||
_notificationManager.ToggleSeen(new NotificationsQuery()
|
||||
{
|
||||
notif.Seen = !notif.Seen;
|
||||
await _db.SaveChangesAsync();
|
||||
_notificationManager.InvalidateNotificationCache(userId);
|
||||
}
|
||||
Ids = new[] {id}, UserId = userId
|
||||
}, true);
|
||||
|
||||
var vm = _notificationManager.ToViewModel(notif);
|
||||
if (string.IsNullOrEmpty(vm.ActionLink))
|
||||
var link = items.FirstOrDefault()?.ActionLink ?? "";
|
||||
if (string.IsNullOrEmpty(link))
|
||||
{
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
return Redirect(vm.ActionLink);
|
||||
return Redirect(link);
|
||||
}
|
||||
|
||||
return NotFound();
|
||||
@ -188,31 +177,29 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
if (selectedItems != null)
|
||||
{
|
||||
var items = _db.Notifications.Where(a => a.ApplicationUserId == userId && selectedItems.Contains(a.Id));
|
||||
switch (command)
|
||||
{
|
||||
case "delete":
|
||||
_db.Notifications.RemoveRange(items);
|
||||
await _notificationManager.Remove(new NotificationsQuery()
|
||||
{
|
||||
UserId = userId, Ids = selectedItems
|
||||
});
|
||||
|
||||
break;
|
||||
case "mark-seen":
|
||||
foreach (NotificationData notificationData in items)
|
||||
await _notificationManager.ToggleSeen(new NotificationsQuery()
|
||||
{
|
||||
notificationData.Seen = true;
|
||||
}
|
||||
UserId = userId, Ids = selectedItems, Seen = false
|
||||
}, true);
|
||||
|
||||
break;
|
||||
case "mark-unseen":
|
||||
foreach (NotificationData notificationData in items)
|
||||
await _notificationManager.ToggleSeen(new NotificationsQuery()
|
||||
{
|
||||
notificationData.Seen = false;
|
||||
}
|
||||
|
||||
UserId = userId, Ids = selectedItems, Seen = true
|
||||
}, false);
|
||||
break;
|
||||
}
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
_notificationManager.InvalidateNotificationCache(userId);
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
|
@ -89,6 +89,8 @@ namespace BTCPayServer.Security.GreenField
|
||||
success = true;
|
||||
}
|
||||
break;
|
||||
case Policies.CanManageNotificationsForUser:
|
||||
case Policies.CanViewNotificationsForUser:
|
||||
case Policies.CanModifyProfile:
|
||||
case Policies.CanViewProfile:
|
||||
case Policies.Unrestricted:
|
||||
|
@ -6,7 +6,6 @@ using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Components.NotificationsDropdown;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.NotificationViewModels;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
@ -38,21 +37,27 @@ namespace BTCPayServer.Services.Notifications
|
||||
{
|
||||
var userId = _userManager.GetUserId(user);
|
||||
var cacheKey = GetNotificationsCacheId(userId);
|
||||
if (_memoryCache.TryGetValue<NotificationSummaryViewModel>(cacheKey, out var obj))
|
||||
return obj;
|
||||
|
||||
var resp = await FetchNotificationsFromDb(userId);
|
||||
_memoryCache.Set(cacheKey, resp,
|
||||
new MemoryCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromMilliseconds(_cacheExpiryMs)));
|
||||
|
||||
return resp;
|
||||
return await _memoryCache.GetOrCreateAsync(cacheKey, async entry =>
|
||||
{
|
||||
var resp = await GetNotifications(new NotificationsQuery()
|
||||
{
|
||||
Seen = false, Skip = 0, Take = 5, UserId = userId
|
||||
});
|
||||
entry.SetAbsoluteExpiration(TimeSpan.FromMilliseconds(_cacheExpiryMs));
|
||||
var res = new NotificationSummaryViewModel() {Last5 = resp.Items, UnseenCount = resp.Count};
|
||||
entry.Value = res;
|
||||
return res;
|
||||
});
|
||||
}
|
||||
|
||||
public void InvalidateNotificationCache(string userId)
|
||||
public void InvalidateNotificationCache(params string[] userIds)
|
||||
{
|
||||
foreach (var userId in userIds)
|
||||
{
|
||||
_memoryCache.Remove(GetNotificationsCacheId(userId));
|
||||
|
||||
_eventAggregator.Publish(new UserNotificationsUpdatedEvent() { UserId = userId });
|
||||
_eventAggregator.Publish(new UserNotificationsUpdatedEvent() {UserId = userId});
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetNotificationsCacheId(string userId)
|
||||
@ -60,52 +65,87 @@ namespace BTCPayServer.Services.Notifications
|
||||
return $"notifications-{userId}";
|
||||
}
|
||||
|
||||
private async Task<NotificationSummaryViewModel> FetchNotificationsFromDb(string userId)
|
||||
public async Task<(List<NotificationViewModel> Items, int Count)> GetNotifications(NotificationsQuery query)
|
||||
{
|
||||
var resp = new NotificationSummaryViewModel();
|
||||
using (var _db = _factory.CreateContext())
|
||||
{
|
||||
resp.UnseenCount = _db.Notifications
|
||||
.Where(a => a.ApplicationUserId == userId && !a.Seen)
|
||||
.Count();
|
||||
await using var dbContext = _factory.CreateContext();
|
||||
|
||||
if (resp.UnseenCount > 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
resp.Last5 = (await _db.Notifications
|
||||
.Where(a => a.ApplicationUserId == userId && !a.Seen)
|
||||
.OrderByDescending(a => a.Created)
|
||||
.Take(5)
|
||||
.ToListAsync())
|
||||
.Select(a => ToViewModel(a))
|
||||
.ToList();
|
||||
}
|
||||
catch (System.IO.InvalidDataException)
|
||||
{
|
||||
// invalid notifications that are not pkuzipable, burn them all
|
||||
var notif = _db.Notifications.Where(a => a.ApplicationUserId == userId);
|
||||
_db.Notifications.RemoveRange(notif);
|
||||
_db.SaveChanges();
|
||||
var queryables = GetNotificationsQueryable(dbContext, query);
|
||||
|
||||
resp.UnseenCount = 0;
|
||||
resp.Last5 = new List<NotificationViewModel>();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
resp.Last5 = new List<NotificationViewModel>();
|
||||
}
|
||||
return (Items: (await queryables.withPaging.ToListAsync()).Select(ToViewModel).ToList(),
|
||||
Count: await queryables.withoutPaging.CountAsync());
|
||||
}
|
||||
|
||||
return resp;
|
||||
private ( IQueryable<NotificationData> withoutPaging, IQueryable<NotificationData> withPaging)
|
||||
GetNotificationsQueryable(ApplicationDbContext dbContext, NotificationsQuery query)
|
||||
{
|
||||
var queryable = dbContext.Notifications.AsQueryable();
|
||||
if (query.Ids?.Any() is true)
|
||||
{
|
||||
queryable = queryable.Where(data => query.Ids.Contains(data.Id));
|
||||
}
|
||||
|
||||
public NotificationViewModel ToViewModel(NotificationData data)
|
||||
if (!string.IsNullOrEmpty(query.UserId))
|
||||
{
|
||||
queryable = queryable.Where(data => data.ApplicationUserId == query.UserId);
|
||||
}
|
||||
|
||||
if (query.Seen.HasValue)
|
||||
{
|
||||
queryable = queryable.Where(data => data.Seen == query.Seen);
|
||||
}
|
||||
|
||||
queryable = queryable.OrderByDescending(a => a.Created);
|
||||
|
||||
var queryable2 = queryable;
|
||||
if (query.Skip.HasValue)
|
||||
{
|
||||
queryable2 = queryable.Skip(query.Skip.Value);
|
||||
}
|
||||
|
||||
if (query.Take.HasValue)
|
||||
{
|
||||
queryable2 = queryable.Take(query.Take.Value);
|
||||
}
|
||||
|
||||
return (queryable, queryable2);
|
||||
}
|
||||
|
||||
|
||||
public async Task<List<NotificationViewModel>> ToggleSeen(NotificationsQuery notificationsQuery, bool? setSeen)
|
||||
{
|
||||
await using var dbContext = _factory.CreateContext();
|
||||
|
||||
var queryables = GetNotificationsQueryable(dbContext, notificationsQuery);
|
||||
var items = await queryables.withPaging.ToListAsync();
|
||||
var userIds = items.Select(data => data.ApplicationUserId).Distinct();
|
||||
foreach (var notificationData in items)
|
||||
{
|
||||
notificationData.Seen = setSeen.GetValueOrDefault(!notificationData.Seen);
|
||||
}
|
||||
|
||||
await dbContext.SaveChangesAsync();
|
||||
InvalidateNotificationCache(userIds.ToArray());
|
||||
return items.Select(ToViewModel).ToList();
|
||||
}
|
||||
|
||||
public async Task Remove(NotificationsQuery notificationsQuery)
|
||||
{
|
||||
await using var dbContext = _factory.CreateContext();
|
||||
|
||||
var queryables = GetNotificationsQueryable(dbContext, notificationsQuery);
|
||||
dbContext.RemoveRange(queryables.withPaging);
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
if (!string.IsNullOrEmpty(notificationsQuery.UserId))
|
||||
InvalidateNotificationCache(notificationsQuery.UserId);
|
||||
}
|
||||
|
||||
|
||||
private NotificationViewModel ToViewModel(NotificationData data)
|
||||
{
|
||||
var handler = GetHandler(data.NotificationType);
|
||||
var notification = JsonConvert.DeserializeObject(ZipUtils.Unzip(data.Blob), handler.NotificationBlobType);
|
||||
var obj = new NotificationViewModel { Id = data.Id, Created = data.Created, Seen = data.Seen };
|
||||
var obj = new NotificationViewModel {Id = data.Id, Created = data.Created, Seen = data.Seen};
|
||||
handler.FillViewModel(notification, obj);
|
||||
return obj;
|
||||
}
|
||||
@ -117,4 +157,13 @@ namespace BTCPayServer.Services.Notifications
|
||||
throw new InvalidOperationException($"No INotificationHandler found for {notificationId}");
|
||||
}
|
||||
}
|
||||
|
||||
public class NotificationsQuery
|
||||
{
|
||||
public string[] Ids { get; set; }
|
||||
public string UserId { get; set; }
|
||||
public int? Skip { get; set; }
|
||||
public int? Take { get; set; }
|
||||
public bool? Seen { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ namespace BTCPayServer.Services.Notifications
|
||||
if (notification == null)
|
||||
throw new ArgumentNullException(nameof(notification));
|
||||
var users = await GetUsers(scope, notification.Identifier);
|
||||
using (var db = _contextFactory.CreateContext())
|
||||
await using (var db = _contextFactory.CreateContext())
|
||||
{
|
||||
foreach (var uid in users)
|
||||
{
|
||||
@ -48,7 +48,7 @@ namespace BTCPayServer.Services.Notifications
|
||||
Blob = ZipUtils.Zip(obj),
|
||||
Seen = false
|
||||
};
|
||||
db.Notifications.Add(data);
|
||||
await db.Notifications.AddAsync(data);
|
||||
}
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
@ -53,7 +53,7 @@
|
||||
"securitySchemes": {
|
||||
"API Key": {
|
||||
"type": "apiKey",
|
||||
"description": "BTCPay Server supports authenticating and authorizing users through an API Key that is generated by them. Send the API Key as a header value to Authorization with the format: `token {token}`. For a smoother experience, you can generate a url that redirects users to an API key creation screen.\n\n The following permissions are available to the context of the user creating the API Key:\n\n* `unrestricted`: Unrestricted access\n* `btcpay.user.canviewprofile`: View your profile\n* `btcpay.user.canmodifyprofile`: Manage your profile\n\nThe following permissions are available if the user is an administrator:\n\n* `btcpay.server.cancreateuser`: Create new users\n* `btcpay.server.canmodifyserversettings`: Manage your server\n* `btcpay.server.canuseinternallightningnode`: Use the internal lightning node\n* `btcpay.server.cancreatelightninginvoiceinternalnode`: Create invoices with internal lightning node\n\nThe following permissions applies to all stores of the user, you can limit to a specific store with the following format: `btcpay.store.cancreateinvoice:6HSHAEU4iYWtjxtyRs9KyPjM9GAQp8kw2T9VWbGG1FnZ`:\n\n* `btcpay.store.canmodifystoresettings`: Modify your stores\n* `btcpay.store.webhooks.canmodifywebhooks`: Modify stores webhooks\n* `btcpay.store.canviewstoresettings`: View your stores\n* `btcpay.store.cancreateinvoice`: Create an invoice\n* `btcpay.store.canviewinvoices`: View invoices\n* `btcpay.store.canmodifypaymentrequests`: Modify your payment requests\n* `btcpay.store.canviewpaymentrequests`: View your payment requests\n* `btcpay.store.canuselightningnode`: Use the lightning nodes associated with your stores\n* `btcpay.store.cancreatelightninginvoice`: Create invoices the lightning nodes associated with your stores\n\nNote that API Keys only limits permission of a user and can never expand it. If an API Key has the permission `btcpay.server.canmodifyserversettings` but that the user account creating this API Key is not administrator, the API Key will not be able to modify the server settings.\n",
|
||||
"description": "BTCPay Server supports authenticating and authorizing users through an API Key that is generated by them. Send the API Key as a header value to Authorization with the format: `token {token}`. For a smoother experience, you can generate a url that redirects users to an API key creation screen.\n\n The following permissions are available to the context of the user creating the API Key:\n\n* `unrestricted`: Unrestricted access\n* `btcpay.user.canviewprofile`: View your profile\n* `btcpay.user.canmodifyprofile`: Manage your profile\n* `btcpay.user.canmanagenotificationsforuser`: Manage your notifications\n* `btcpay.user.canviewnotificationsforuser`: View your notifications\n\nThe following permissions are available if the user is an administrator:\n\n* `btcpay.server.cancreateuser`: Create new users\n* `btcpay.server.canmodifyserversettings`: Manage your server\n* `btcpay.server.canuseinternallightningnode`: Use the internal lightning node\n* `btcpay.server.cancreatelightninginvoiceinternalnode`: Create invoices with internal lightning node\n\nThe following permissions applies to all stores of the user, you can limit to a specific store with the following format: `btcpay.store.cancreateinvoice:6HSHAEU4iYWtjxtyRs9KyPjM9GAQp8kw2T9VWbGG1FnZ`:\n\n* `btcpay.store.canmodifystoresettings`: Modify your stores\n* `btcpay.store.webhooks.canmodifywebhooks`: Modify stores webhooks\n* `btcpay.store.canviewstoresettings`: View your stores\n* `btcpay.store.cancreateinvoice`: Create an invoice\n* `btcpay.store.canviewinvoices`: View invoices\n* `btcpay.store.canmodifypaymentrequests`: Modify your payment requests\n* `btcpay.store.canviewpaymentrequests`: View your payment requests\n* `btcpay.store.canuselightningnode`: Use the lightning nodes associated with your stores\n* `btcpay.store.cancreatelightninginvoice`: Create invoices the lightning nodes associated with your stores\n\nNote that API Keys only limits permission of a user and can never expand it. If an API Key has the permission `btcpay.server.canmodifyserversettings` but that the user account creating this API Key is not administrator, the API Key will not be able to modify the server settings.\n",
|
||||
"name": "Authorization",
|
||||
"in": "header",
|
||||
"scheme": "token"
|
||||
|
@ -0,0 +1,238 @@
|
||||
{
|
||||
"paths": {
|
||||
"/api/v1/users/me/notifications": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Notifications (Current User)"
|
||||
],
|
||||
"summary": "Get notifications",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "seen",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "filter by seen notifications",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"description": "View current user's notifications",
|
||||
"operationId": "Notifications_GetNotifications",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "list of notifications",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/NotificationData"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"API Key": [
|
||||
"btcpay.user.canmanagenotificationsforuser",
|
||||
"btcpay.user.canviewnotificationsforuser"
|
||||
],
|
||||
"Basic": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v1/users/me/notifications/{id}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Notifications (Current User)"
|
||||
],
|
||||
"summary": "Get notification",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The notification to fetch",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"description": "View information about the specified notification",
|
||||
"operationId": "Notifications_GetInvoice",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "specified notification",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/NotificationData"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "If you are authenticated but forbidden to view the specified notification"
|
||||
},
|
||||
"404": {
|
||||
"description": "The key is not found for this notification"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"API Key": [
|
||||
"btcpay.user.canmanagenotificationsforuser",
|
||||
"btcpay.user.canviewnotificationsforuser"
|
||||
],
|
||||
"Basic": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"put": {
|
||||
"tags": [
|
||||
"Notifications (Current User)"
|
||||
],
|
||||
"summary": "Update notification",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The notification to update",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"description": "Updates the notification",
|
||||
"operationId": "Notifications_UpdateNotification",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "updated notification",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/NotificationData"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "If you are authenticated but forbidden to update the specified notification"
|
||||
},
|
||||
"404": {
|
||||
"description": "The key is not found for this notification"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"API Key": [
|
||||
"btcpay.user.canmanagenotificationsforuser"
|
||||
],
|
||||
"Basic": []
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdateNotification"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"Notifications (Current User)"
|
||||
],
|
||||
"summary": "Remove Notification",
|
||||
"description": "Removes the specified notification.",
|
||||
"operationId": "Notifications_DeleteNotification",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The notification to remove",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The notification has been deleted"
|
||||
},
|
||||
"403": {
|
||||
"description": "If you are authenticated but forbidden to remove the specified notification"
|
||||
},
|
||||
"404": {
|
||||
"description": "The key is not found for this notification"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"API Key": [
|
||||
"btcpay.user.canmanagenotificationsforuser"
|
||||
],
|
||||
"Basic": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"UpdateNotification": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"seen": {
|
||||
"type": "boolean",
|
||||
"nullable": true,
|
||||
"description": "Sets the notification as seen/unseen. If left null, sets it to the opposite value"
|
||||
}
|
||||
}
|
||||
},
|
||||
"NotificationData": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "The identifier of the notification"
|
||||
},
|
||||
"body": {
|
||||
"type": "string",
|
||||
"format": "html",
|
||||
"description": "The html body of the notifications"
|
||||
},
|
||||
"link": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"nullable": true,
|
||||
"description": "The link of the notification"
|
||||
},
|
||||
"createdTime": {
|
||||
"type": "number",
|
||||
"format": "int64",
|
||||
"description": "The creation time of the notification"
|
||||
},
|
||||
"seen": {
|
||||
"type": "boolean",
|
||||
"description": "If the notification has been seen by the user"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
{
|
||||
"name": "Notifications (Current User)"
|
||||
}
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue
Block a user