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:
Andrew Camilleri 2020-12-11 15:11:08 +01:00 committed by GitHub
parent 1c58fabc30
commit 0652e30c30
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 606 additions and 109 deletions

View 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);
}
}
}

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

View File

@ -0,0 +1,7 @@
namespace BTCPayServer.Client.Models
{
public class UpdateNotification
{
public bool? Seen { get; set; }
}
}

View File

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

View File

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

View File

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

View 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)
};
}
}
}

View File

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

View File

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

View File

@ -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.")},

View File

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

View File

@ -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:

View File

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

View File

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

View File

@ -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"

View File

@ -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)"
}
]
}