App: Add events which the app subscribes to (#6435)

* App: Add events which the app subscribes to

Various events, which are relevant for the app to react to changes made on the server.

* Refactor events

* Do not extend NewBlockEvent

* Refactoring events

* Add store role events

* Refactoring: Rename StoreUserEvent

* Fix: Subscribe to UserEvent.Invited

---------

Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
This commit is contained in:
d11n 2024-12-11 12:11:51 +01:00 committed by GitHub
parent 6e222c573b
commit 00cc16455c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 493 additions and 326 deletions

View file

@ -3829,7 +3829,6 @@ namespace BTCPayServer.Tests
await tester.WaitForEvent<NewBlockEvent>(async () => await tester.WaitForEvent<NewBlockEvent>(async () =>
{ {
await tester.ExplorerNode.GenerateAsync(1); await tester.ExplorerNode.GenerateAsync(1);
}, bevent => bevent.PaymentMethodId == PaymentTypes.CHAIN.GetPaymentMethodId("BTC")); }, bevent => bevent.PaymentMethodId == PaymentTypes.CHAIN.GetPaymentMethodId("BTC"));

View file

@ -37,6 +37,7 @@ namespace BTCPayServer.Controllers.Greenfield
private readonly RoleManager<IdentityRole> _roleManager; private readonly RoleManager<IdentityRole> _roleManager;
private readonly SettingsRepository _settingsRepository; private readonly SettingsRepository _settingsRepository;
private readonly EventAggregator _eventAggregator; private readonly EventAggregator _eventAggregator;
private readonly CallbackGenerator _callbackGenerator;
private readonly IPasswordValidator<ApplicationUser> _passwordValidator; private readonly IPasswordValidator<ApplicationUser> _passwordValidator;
private readonly IRateLimitService _throttleService; private readonly IRateLimitService _throttleService;
private readonly BTCPayServerOptions _options; private readonly BTCPayServerOptions _options;
@ -50,6 +51,7 @@ namespace BTCPayServer.Controllers.Greenfield
SettingsRepository settingsRepository, SettingsRepository settingsRepository,
PoliciesSettings policiesSettings, PoliciesSettings policiesSettings,
EventAggregator eventAggregator, EventAggregator eventAggregator,
CallbackGenerator callbackGenerator,
IPasswordValidator<ApplicationUser> passwordValidator, IPasswordValidator<ApplicationUser> passwordValidator,
IRateLimitService throttleService, IRateLimitService throttleService,
BTCPayServerOptions options, BTCPayServerOptions options,
@ -65,6 +67,7 @@ namespace BTCPayServer.Controllers.Greenfield
_settingsRepository = settingsRepository; _settingsRepository = settingsRepository;
PoliciesSettings = policiesSettings; PoliciesSettings = policiesSettings;
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
_callbackGenerator = callbackGenerator;
_passwordValidator = passwordValidator; _passwordValidator = passwordValidator;
_throttleService = throttleService; _throttleService = throttleService;
_options = options; _options = options;
@ -113,7 +116,8 @@ namespace BTCPayServer.Controllers.Greenfield
if (user.RequiresApproval) if (user.RequiresApproval)
{ {
return await _userService.SetUserApproval(user.Id, request.Approved, Request.GetAbsoluteRootUri()) var loginLink = _callbackGenerator.ForLogin(user, Request);
return await _userService.SetUserApproval(user.Id, request.Approved, loginLink)
? Ok() ? Ok()
: this.CreateAPIError("invalid-state", $"User is already {(request.Approved ? "approved" : "unapproved")}"); : this.CreateAPIError("invalid-state", $"User is already {(request.Approved ? "approved" : "unapproved")}");
} }
@ -219,6 +223,10 @@ namespace BTCPayServer.Controllers.Greenfield
ModelState.AddModelError(string.Empty, error.Description); ModelState.AddModelError(string.Empty, error.Description);
} }
} }
else
{
_eventAggregator.Publish(new UserEvent.Updated(user));
}
} }
if (!ModelState.IsValid) if (!ModelState.IsValid)
@ -255,7 +263,7 @@ namespace BTCPayServer.Controllers.Greenfield
blob.ImageUrl = fileIdUri.ToString(); blob.ImageUrl = fileIdUri.ToString();
user.SetBlob(blob); user.SetBlob(blob);
await _userManager.UpdateAsync(user); await _userManager.UpdateAsync(user);
_eventAggregator.Publish(new UserEvent.Updated(user));
var model = await FromModel(user); var model = await FromModel(user);
return Ok(model); return Ok(model);
} }
@ -280,6 +288,7 @@ namespace BTCPayServer.Controllers.Greenfield
blob.ImageUrl = null; blob.ImageUrl = null;
user.SetBlob(blob); user.SetBlob(blob);
await _userManager.UpdateAsync(user); await _userManager.UpdateAsync(user);
_eventAggregator.Publish(new UserEvent.Updated(user));
} }
return Ok(); return Ok();
} }
@ -399,18 +408,11 @@ namespace BTCPayServer.Controllers.Greenfield
await _settingsRepository.FirstAdminRegistered(policies, _options.UpdateUrl != null, _options.DisableRegistration, Logs); await _settingsRepository.FirstAdminRegistered(policies, _options.UpdateUrl != null, _options.DisableRegistration, Logs);
} }
} }
var currentUser = await _userManager.GetUserAsync(User); var currentUser = await _userManager.GetUserAsync(User);
var userEvent = new UserRegisteredEvent var userEvent = currentUser switch
{ {
RequestUri = Request.GetAbsoluteRootUri(), { } invitedBy => await UserEvent.Invited.Create(user, invitedBy, _callbackGenerator, Request, true),
Admin = isNewAdmin, _ => await UserEvent.Registered.Create(user, _callbackGenerator, Request)
User = user
};
if (currentUser is not null)
{
userEvent.Kind = UserRegisteredEventKind.Invite;
userEvent.InvitedByUser = currentUser;
}; };
_eventAggregator.Publish(userEvent); _eventAggregator.Publish(userEvent);
@ -444,6 +446,7 @@ namespace BTCPayServer.Controllers.Greenfield
// Ok, this user is an admin but there are other admins as well so safe to delete // Ok, this user is an admin but there are other admins as well so safe to delete
await _userService.DeleteUserAndAssociatedData(user); await _userService.DeleteUserAndAssociatedData(user);
_eventAggregator.Publish(new UserEvent.Deleted(user));
return Ok(); return Ok();
} }

View file

@ -41,7 +41,7 @@ namespace BTCPayServer.Controllers
readonly SettingsRepository _SettingsRepository; readonly SettingsRepository _SettingsRepository;
private readonly Fido2Service _fido2Service; private readonly Fido2Service _fido2Service;
private readonly LnurlAuthService _lnurlAuthService; private readonly LnurlAuthService _lnurlAuthService;
private readonly LinkGenerator _linkGenerator; private readonly CallbackGenerator _callbackGenerator;
private readonly UserLoginCodeService _userLoginCodeService; private readonly UserLoginCodeService _userLoginCodeService;
private readonly EventAggregator _eventAggregator; private readonly EventAggregator _eventAggregator;
readonly ILogger _logger; readonly ILogger _logger;
@ -64,7 +64,7 @@ namespace BTCPayServer.Controllers
UserLoginCodeService userLoginCodeService, UserLoginCodeService userLoginCodeService,
LnurlAuthService lnurlAuthService, LnurlAuthService lnurlAuthService,
EmailSenderFactory emailSenderFactory, EmailSenderFactory emailSenderFactory,
LinkGenerator linkGenerator, CallbackGenerator callbackGenerator,
IStringLocalizer stringLocalizer, IStringLocalizer stringLocalizer,
Logs logs) Logs logs)
{ {
@ -78,8 +78,8 @@ namespace BTCPayServer.Controllers
_fido2Service = fido2Service; _fido2Service = fido2Service;
_lnurlAuthService = lnurlAuthService; _lnurlAuthService = lnurlAuthService;
EmailSenderFactory = emailSenderFactory; EmailSenderFactory = emailSenderFactory;
_linkGenerator = linkGenerator; _callbackGenerator = callbackGenerator;
_userLoginCodeService = userLoginCodeService; _userLoginCodeService = userLoginCodeService;
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
_logger = logs.PayServer; _logger = logs.PayServer;
Logs = logs; Logs = logs;
@ -297,10 +297,7 @@ namespace BTCPayServer.Controllers
{ {
RememberMe = rememberMe, RememberMe = rememberMe,
UserId = user.Id, UserId = user.Id,
LNURLEndpoint = new Uri(_linkGenerator.GetUriByAction( LNURLEndpoint = new Uri(_callbackGenerator.ForLNUrlAuth(user, r, Request))
action: nameof(UILNURLAuthController.LoginResponse),
controller: "UILNURLAuth",
values: new { userId = user.Id, action = "login", tag = "login", k1 = Encoders.Hex.EncodeData(r) }, Request.Scheme, Request.Host, Request.PathBase) ?? string.Empty)
}; };
} }
return null; return null;
@ -627,12 +624,7 @@ namespace BTCPayServer.Controllers
RegisteredAdmin = true; RegisteredAdmin = true;
} }
_eventAggregator.Publish(new UserRegisteredEvent _eventAggregator.Publish(await UserEvent.Registered.Create(user, _callbackGenerator, Request));
{
RequestUri = Request.GetAbsoluteRootUri(),
User = user,
Admin = RegisteredAdmin
});
RegisteredUserId = user.Id; RegisteredUserId = user.Id;
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Account created."].Value; TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Account created."].Value;
@ -699,11 +691,8 @@ namespace BTCPayServer.Controllers
var result = await _userManager.ConfirmEmailAsync(user, code); var result = await _userManager.ConfirmEmailAsync(user, code);
if (result.Succeeded) if (result.Succeeded)
{ {
_eventAggregator.Publish(new UserConfirmedEmailEvent var approvalLink = _callbackGenerator.ForApproval(user, Request);
{ _eventAggregator.Publish(new UserEvent.ConfirmedEmail(user, approvalLink));
User = user,
RequestUri = Request.GetAbsoluteRootUri()
});
var hasPassword = await _userManager.HasPasswordAsync(user); var hasPassword = await _userManager.HasPasswordAsync(user);
if (hasPassword) if (hasPassword)
@ -749,11 +738,8 @@ namespace BTCPayServer.Controllers
// Don't reveal that the user does not exist or is not confirmed // Don't reveal that the user does not exist or is not confirmed
return RedirectToAction(nameof(ForgotPasswordConfirmation)); return RedirectToAction(nameof(ForgotPasswordConfirmation));
} }
_eventAggregator.Publish(new UserPasswordResetRequestedEvent var callbackUri = await _callbackGenerator.ForPasswordReset(user, Request);
{ _eventAggregator.Publish(new UserEvent.PasswordResetRequested(user, callbackUri));
User = user,
RequestUri = Request.GetAbsoluteRootUri()
});
return RedirectToAction(nameof(ForgotPasswordConfirmation)); return RedirectToAction(nameof(ForgotPasswordConfirmation));
} }
@ -889,11 +875,10 @@ namespace BTCPayServer.Controllers
private async Task FinalizeInvitationIfApplicable(ApplicationUser user) private async Task FinalizeInvitationIfApplicable(ApplicationUser user)
{ {
if (!_userManager.HasInvitationToken<ApplicationUser>(user)) return; if (!_userManager.HasInvitationToken<ApplicationUser>(user)) return;
_eventAggregator.Publish(new UserInviteAcceptedEvent
{ // This is a placeholder, the real storeIds will be set by the UserEventHostedService
User = user, var storeUsersLink = _callbackGenerator.StoreUsersLink("{0}", Request);
RequestUri = Request.GetAbsoluteRootUri() _eventAggregator.Publish(new UserEvent.InviteAccepted(user, storeUsersLink));
});
// unset used token // unset used token
await _userManager.UnsetInvitationTokenAsync<ApplicationUser>(user.Id); await _userManager.UnsetInvitationTokenAsync<ApplicationUser>(user.Id);
} }

View file

@ -4,9 +4,9 @@ using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants; using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Contracts; using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Extensions; using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client; using BTCPayServer.Client;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Fido2; using BTCPayServer.Fido2;
using BTCPayServer.Models.ManageViewModels; using BTCPayServer.Models.ManageViewModels;
using BTCPayServer.Security.Greenfield; using BTCPayServer.Security.Greenfield;
@ -36,11 +36,12 @@ namespace BTCPayServer.Controllers
private readonly APIKeyRepository _apiKeyRepository; private readonly APIKeyRepository _apiKeyRepository;
private readonly IAuthorizationService _authorizationService; private readonly IAuthorizationService _authorizationService;
private readonly Fido2Service _fido2Service; private readonly Fido2Service _fido2Service;
private readonly LinkGenerator _linkGenerator; private readonly CallbackGenerator _callbackGenerator;
private readonly IHtmlHelper Html; private readonly IHtmlHelper Html;
private readonly UserService _userService; private readonly UserService _userService;
private readonly UriResolver _uriResolver; private readonly UriResolver _uriResolver;
private readonly IFileService _fileService; private readonly IFileService _fileService;
private readonly EventAggregator _eventAggregator;
readonly StoreRepository _StoreRepository; readonly StoreRepository _StoreRepository;
public IStringLocalizer StringLocalizer { get; } public IStringLocalizer StringLocalizer { get; }
@ -55,13 +56,13 @@ namespace BTCPayServer.Controllers
APIKeyRepository apiKeyRepository, APIKeyRepository apiKeyRepository,
IAuthorizationService authorizationService, IAuthorizationService authorizationService,
Fido2Service fido2Service, Fido2Service fido2Service,
LinkGenerator linkGenerator, CallbackGenerator callbackGenerator,
UserService userService, UserService userService,
UriResolver uriResolver, UriResolver uriResolver,
IFileService fileService, IFileService fileService,
IStringLocalizer stringLocalizer, IStringLocalizer stringLocalizer,
IHtmlHelper htmlHelper IHtmlHelper htmlHelper,
) EventAggregator eventAggregator)
{ {
_userManager = userManager; _userManager = userManager;
_signInManager = signInManager; _signInManager = signInManager;
@ -72,8 +73,9 @@ namespace BTCPayServer.Controllers
_apiKeyRepository = apiKeyRepository; _apiKeyRepository = apiKeyRepository;
_authorizationService = authorizationService; _authorizationService = authorizationService;
_fido2Service = fido2Service; _fido2Service = fido2Service;
_linkGenerator = linkGenerator; _callbackGenerator = callbackGenerator;
Html = htmlHelper; Html = htmlHelper;
_eventAggregator = eventAggregator;
_userService = userService; _userService = userService;
_uriResolver = uriResolver; _uriResolver = uriResolver;
_fileService = fileService; _fileService = fileService;
@ -189,9 +191,9 @@ namespace BTCPayServer.Controllers
return View(model); return View(model);
} }
if (needUpdate is true) if (needUpdate && await _userManager.UpdateAsync(user) is { Succeeded: true })
{ {
needUpdate = await _userManager.UpdateAsync(user) is { Succeeded: true }; _eventAggregator.Publish(new UserEvent.Updated(user));
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Your profile has been updated"].Value; TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Your profile has been updated"].Value;
} }
else else
@ -217,8 +219,7 @@ namespace BTCPayServer.Controllers
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
} }
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); var callbackUrl = await _callbackGenerator.ForEmailConfirmation(user, Request);
var callbackUrl = _linkGenerator.EmailConfirmationLink(user.Id, code, Request.Scheme, Request.Host, Request.PathBase);
(await _EmailSenderFactory.GetEmailSender()).SendEmailConfirmation(user.GetMailboxAddress(), callbackUrl); (await _EmailSenderFactory.GetEmailSender()).SendEmailConfirmation(user.GetMailboxAddress(), callbackUrl);
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Verification email sent. Please check your email."].Value; TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Verification email sent. Please check your email."].Value;
return RedirectToAction(nameof(Index)); return RedirectToAction(nameof(Index));
@ -320,7 +321,7 @@ namespace BTCPayServer.Controllers
return RedirectToAction(nameof(SetPassword)); return RedirectToAction(nameof(SetPassword));
} }
[HttpPost()] [HttpPost]
public async Task<IActionResult> DeleteUserPost() public async Task<IActionResult> DeleteUserPost()
{ {
var user = await _userManager.GetUserAsync(User); var user = await _userManager.GetUserAsync(User);
@ -330,12 +331,12 @@ namespace BTCPayServer.Controllers
} }
await _userService.DeleteUserAndAssociatedData(user); await _userService.DeleteUserAndAssociatedData(user);
_eventAggregator.Publish(new UserEvent.Deleted(user));
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Account successfully deleted."].Value; TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Account successfully deleted."].Value;
await _signInManager.SignOutAsync(); await _signInManager.SignOutAsync();
return RedirectToAction(nameof(UIAccountController.Login), "UIAccount"); return RedirectToAction(nameof(UIAccountController.Login), "UIAccount");
} }
#region Helpers #region Helpers
private void AddErrors(IdentityResult result) private void AddErrors(IdentityResult result)

View file

@ -70,8 +70,7 @@ namespace BTCPayServer.Controllers
InvitationUrl = InvitationUrl =
string.IsNullOrEmpty(blob?.InvitationToken) string.IsNullOrEmpty(blob?.InvitationToken)
? null ? null
: _linkGenerator.InvitationLink(u.Id, blob.InvitationToken, Request.Scheme, : _callbackGenerator.ForInvitation(u, blob.InvitationToken, Request),
Request.Host, Request.PathBase),
EmailConfirmed = u.RequiresEmailConfirmation ? u.EmailConfirmed : null, EmailConfirmed = u.RequiresEmailConfirmation ? u.EmailConfirmed : null,
Approved = u.RequiresApproval ? u.Approved : null, Approved = u.RequiresApproval ? u.Approved : null,
Created = u.Created, Created = u.Created,
@ -98,7 +97,7 @@ namespace BTCPayServer.Controllers
Id = user.Id, Id = user.Id,
Email = user.Email, Email = user.Email,
Name = blob?.Name, Name = blob?.Name,
InvitationUrl = string.IsNullOrEmpty(blob?.InvitationToken) ? null : _linkGenerator.InvitationLink(user.Id, blob.InvitationToken, Request.Scheme, Request.Host, Request.PathBase), InvitationUrl = string.IsNullOrEmpty(blob?.InvitationToken) ? null : _callbackGenerator.ForInvitation(user, blob.InvitationToken, Request),
ImageUrl = string.IsNullOrEmpty(blob?.ImageUrl) ? null : await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), UnresolvedUri.Create(blob.ImageUrl)), ImageUrl = string.IsNullOrEmpty(blob?.ImageUrl) ? null : await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), UnresolvedUri.Create(blob.ImageUrl)),
EmailConfirmed = user.RequiresEmailConfirmation ? user.EmailConfirmed : null, EmailConfirmed = user.RequiresEmailConfirmation ? user.EmailConfirmed : null,
Approved = user.RequiresApproval ? user.Approved : null, Approved = user.RequiresApproval ? user.Approved : null,
@ -120,7 +119,8 @@ namespace BTCPayServer.Controllers
if (user.RequiresApproval && viewModel.Approved.HasValue && user.Approved != viewModel.Approved.Value) if (user.RequiresApproval && viewModel.Approved.HasValue && user.Approved != viewModel.Approved.Value)
{ {
approvalStatusChanged = await _userService.SetUserApproval(user.Id, viewModel.Approved.Value, Request.GetAbsoluteRootUri()); var loginLink = _callbackGenerator.ForLogin(user, Request);
approvalStatusChanged = await _userService.SetUserApproval(user.Id, viewModel.Approved.Value, loginLink);
} }
if (user.RequiresEmailConfirmation && viewModel.EmailConfirmed.HasValue && user.EmailConfirmed != viewModel.EmailConfirmed) if (user.RequiresEmailConfirmation && viewModel.EmailConfirmed.HasValue && user.EmailConfirmed != viewModel.EmailConfirmed)
{ {
@ -260,31 +260,21 @@ namespace BTCPayServer.Controllers
if (model.IsAdmin && !(await _UserManager.AddToRoleAsync(user, Roles.ServerAdmin)).Succeeded) if (model.IsAdmin && !(await _UserManager.AddToRoleAsync(user, Roles.ServerAdmin)).Succeeded)
model.IsAdmin = false; model.IsAdmin = false;
var tcs = new TaskCompletionSource<Uri>();
var currentUser = await _UserManager.GetUserAsync(HttpContext.User); var currentUser = await _UserManager.GetUserAsync(HttpContext.User);
var sendEmail = model.SendInvitationEmail && ViewData["CanSendEmail"] is true; var sendEmail = model.SendInvitationEmail && ViewData["CanSendEmail"] is true;
_eventAggregator.Publish(new UserRegisteredEvent var evt = await UserEvent.Invited.Create(user, currentUser, _callbackGenerator, Request, sendEmail);
{ _eventAggregator.Publish(evt);
RequestUri = Request.GetAbsoluteRootUri(),
Kind = UserRegisteredEventKind.Invite,
User = user,
InvitedByUser = currentUser,
SendInvitationEmail = sendEmail,
Admin = model.IsAdmin,
CallbackUrlGenerated = tcs
});
var callbackUrl = await tcs.Task;
var info = sendEmail var info = sendEmail
? "An invitation email has been sent. You may alternatively" ? "An invitation email has been sent. You may alternatively"
: "An invitation email has not been sent. You need to"; : "An invitation email has not been sent. You need to";
TempData.SetStatusMessageModel(new StatusMessageModel TempData.SetStatusMessageModel(new StatusMessageModel
{ {
Severity = StatusMessageModel.StatusSeverity.Success, Severity = StatusMessageModel.StatusSeverity.Success,
AllowDismiss = false, AllowDismiss = false,
Html = $"Account successfully created. {info} share this link with them:<br/>{callbackUrl}" Html = $"Account successfully created. {info} share this link with them:<br/>{evt.InvitationLink}"
}); });
return RedirectToAction(nameof(User), new { userId = user.Id }); return RedirectToAction(nameof(User), new { userId = user.Id });
} }
@ -387,7 +377,8 @@ namespace BTCPayServer.Controllers
if (user == null) if (user == null)
return NotFound(); return NotFound();
await _userService.SetUserApproval(userId, approved, Request.GetAbsoluteRootUri()); var loginLink = _callbackGenerator.ForLogin(user, Request);
await _userService.SetUserApproval(userId, approved, loginLink);
TempData[WellKnownTempData.SuccessMessage] = approved TempData[WellKnownTempData.SuccessMessage] = approved
? StringLocalizer["User approved"].Value ? StringLocalizer["User approved"].Value
@ -414,8 +405,7 @@ namespace BTCPayServer.Controllers
throw new ApplicationException($"Unable to load user with ID '{userId}'."); throw new ApplicationException($"Unable to load user with ID '{userId}'.");
} }
var code = await _UserManager.GenerateEmailConfirmationTokenAsync(user); var callbackUrl = await _callbackGenerator.ForEmailConfirmation(user, Request);
var callbackUrl = _linkGenerator.EmailConfirmationLink(user.Id, code, Request.Scheme, Request.Host, Request.PathBase);
(await _emailSenderFactory.GetEmailSender()).SendEmailConfirmation(user.GetMailboxAddress(), callbackUrl); (await _emailSenderFactory.GetEmailSender()).SendEmailConfirmation(user.GetMailboxAddress(), callbackUrl);

View file

@ -64,7 +64,7 @@ namespace BTCPayServer.Controllers
private readonly StoredFileRepository _StoredFileRepository; private readonly StoredFileRepository _StoredFileRepository;
private readonly IFileService _fileService; private readonly IFileService _fileService;
private readonly IEnumerable<IStorageProviderService> _StorageProviderServices; private readonly IEnumerable<IStorageProviderService> _StorageProviderServices;
private readonly LinkGenerator _linkGenerator; private readonly CallbackGenerator _callbackGenerator;
private readonly UriResolver _uriResolver; private readonly UriResolver _uriResolver;
private readonly EmailSenderFactory _emailSenderFactory; private readonly EmailSenderFactory _emailSenderFactory;
private readonly TransactionLinkProviders _transactionLinkProviders; private readonly TransactionLinkProviders _transactionLinkProviders;
@ -90,7 +90,7 @@ namespace BTCPayServer.Controllers
EventAggregator eventAggregator, EventAggregator eventAggregator,
IOptions<ExternalServicesOptions> externalServiceOptions, IOptions<ExternalServicesOptions> externalServiceOptions,
Logs logs, Logs logs,
LinkGenerator linkGenerator, CallbackGenerator callbackGenerator,
UriResolver uriResolver, UriResolver uriResolver,
EmailSenderFactory emailSenderFactory, EmailSenderFactory emailSenderFactory,
IHostApplicationLifetime applicationLifetime, IHostApplicationLifetime applicationLifetime,
@ -119,7 +119,7 @@ namespace BTCPayServer.Controllers
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
_externalServiceOptions = externalServiceOptions; _externalServiceOptions = externalServiceOptions;
Logs = logs; Logs = logs;
_linkGenerator = linkGenerator; _callbackGenerator = callbackGenerator;
_uriResolver = uriResolver; _uriResolver = uriResolver;
_emailSenderFactory = emailSenderFactory; _emailSenderFactory = emailSenderFactory;
ApplicationLifetime = applicationLifetime; ApplicationLifetime = applicationLifetime;

View file

@ -9,6 +9,7 @@ using BTCPayServer.Client;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Events; using BTCPayServer.Events;
using BTCPayServer.Models.StoreViewModels; using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Services;
using BTCPayServer.Services.Mails; using BTCPayServer.Services.Mails;
using BTCPayServer.Services.Stores; using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@ -58,28 +59,18 @@ public partial class UIStoresController
Created = DateTimeOffset.UtcNow Created = DateTimeOffset.UtcNow
}; };
var result = await _userManager.CreateAsync(user); var currentUser = await _userManager.GetUserAsync(HttpContext.User);
if (result.Succeeded) if (currentUser is not null &&
(await _userManager.CreateAsync(user)) is { Succeeded: true } result)
{ {
var invitationEmail = await _emailSenderFactory.IsComplete(); var invitationEmail = await _emailSenderFactory.IsComplete();
var tcs = new TaskCompletionSource<Uri>(); var evt = await UserEvent.Invited.Create(user, currentUser, _callbackGenerator, Request, invitationEmail);
var currentUser = await _userManager.GetUserAsync(HttpContext.User); _eventAggregator.Publish(evt);
_eventAggregator.Publish(new UserRegisteredEvent
{
RequestUri = Request.GetAbsoluteRootUri(),
Kind = UserRegisteredEventKind.Invite,
User = user,
InvitedByUser = currentUser,
SendInvitationEmail = invitationEmail,
CallbackUrlGenerated = tcs
});
var callbackUrl = await tcs.Task;
var info = invitationEmail var info = invitationEmail
? "An invitation email has been sent.<br/>You may alternatively" ? "An invitation email has been sent.<br/>You may alternatively"
: "An invitation email has not been sent, because the server does not have an email server configured.<br/> You need to"; : "An invitation email has not been sent, because the server does not have an email server configured.<br/> You need to";
successInfo = $"{info} share this link with them: <a class='alert-link' href='{callbackUrl}'>{callbackUrl}</a>"; successInfo = $"{info} share this link with them: <a class='alert-link' href='{evt.InvitationLink}'>{evt.InvitationLink}</a>";
} }
else else
{ {

View file

@ -59,6 +59,7 @@ public partial class UIStoresController : Controller
EmailSenderFactory emailSenderFactory, EmailSenderFactory emailSenderFactory,
WalletFileParsers onChainWalletParsers, WalletFileParsers onChainWalletParsers,
UIUserStoresController userStoresController, UIUserStoresController userStoresController,
CallbackGenerator callbackGenerator,
UriResolver uriResolver, UriResolver uriResolver,
CurrencyNameTable currencyNameTable, CurrencyNameTable currencyNameTable,
IStringLocalizer stringLocalizer, IStringLocalizer stringLocalizer,
@ -86,6 +87,7 @@ public partial class UIStoresController : Controller
_emailSenderFactory = emailSenderFactory; _emailSenderFactory = emailSenderFactory;
_onChainWalletParsers = onChainWalletParsers; _onChainWalletParsers = onChainWalletParsers;
_userStoresController = userStoresController; _userStoresController = userStoresController;
_callbackGenerator = callbackGenerator;
_uriResolver = uriResolver; _uriResolver = uriResolver;
_currencyNameTable = currencyNameTable; _currencyNameTable = currencyNameTable;
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
@ -121,6 +123,7 @@ public partial class UIStoresController : Controller
private readonly EmailSenderFactory _emailSenderFactory; private readonly EmailSenderFactory _emailSenderFactory;
private readonly WalletFileParsers _onChainWalletParsers; private readonly WalletFileParsers _onChainWalletParsers;
private readonly UIUserStoresController _userStoresController; private readonly UIUserStoresController _userStoresController;
private readonly CallbackGenerator _callbackGenerator;
private readonly UriResolver _uriResolver; private readonly UriResolver _uriResolver;
private readonly EventAggregator _eventAggregator; private readonly EventAggregator _eventAggregator;
private readonly IHtmlHelper _html; private readonly IHtmlHelper _html;

View file

@ -0,0 +1,37 @@
#nullable enable
using BTCPayServer.Data;
namespace BTCPayServer.Events;
public class AppEvent(AppData app, string? detail = null)
{
public class Created(AppData app, string? detail = null) : AppEvent(app, detail ?? app.AppType)
{
protected override string ToString()
{
return $"{base.ToString()} has been created";
}
}
public class Deleted(AppData app, string? detail = null) : AppEvent(app, detail ?? app.AppType)
{
protected override string ToString()
{
return $"{base.ToString()} has been deleted";
}
}
public class Updated(AppData app, string? detail = null) : AppEvent(app, detail ?? app.AppType)
{
protected override string ToString()
{
return $"{base.ToString()} has been updated";
}
}
public string AppId { get; } = app.Id;
public string StoreId { get; } = app.StoreDataId;
public string? Detail { get; } = detail;
protected new virtual string ToString()
{
return $"AppEvent: App \"{app.Name}\" ({StoreId})";
}
}

View file

@ -5,9 +5,11 @@ namespace BTCPayServer.Events
public class NewBlockEvent public class NewBlockEvent
{ {
public PaymentMethodId PaymentMethodId { get; set; } public PaymentMethodId PaymentMethodId { get; set; }
public object AdditionalInfo { get; set; }
public override string ToString() public override string ToString()
{ {
return $"{PaymentMethodId}: New block"; return $"{PaymentMethodId}: New block";
} }
} }
} }

View file

@ -0,0 +1,50 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
using BTCPayServer.Data;
namespace BTCPayServer.Events;
public class StoreEvent(StoreData store, string? detail = null)
{
public class Created(StoreData store, string? detail = null) : StoreEvent(store, detail)
{
protected override string ToString()
{
return $"{base.ToString()} has been created";
}
}
public class Removed(StoreData store, string? detail = null) : StoreEvent(store, detail)
{
protected override string ToString()
{
return $"{base.ToString()} has been removed";
}
}
public class Updated(StoreData store, string? detail = null) : StoreEvent(store, detail)
{
protected override string ToString()
{
return $"{base.ToString()} has been updated";
}
}
public string StoreId { get; } = store.Id;
public string? Detail { get; } = detail;
public IEnumerable<StoreUser>? StoreUsers { get; } = store.UserStores?.Select(userStore => new StoreUser
{
UserId = userStore.ApplicationUserId,
RoleId = userStore.StoreRoleId
});
protected new virtual string ToString()
{
return $"StoreEvent: Store \"{store.StoreName}\" ({store.Id})";
}
public class StoreUser
{
public string UserId { get; init; } = null!;
public string RoleId { get; set; } = null!;
}
}

View file

@ -1,14 +0,0 @@
namespace BTCPayServer.Events;
public class StoreRemovedEvent
{
public StoreRemovedEvent(string storeId)
{
StoreId = storeId;
}
public string StoreId { get; set; }
public override string ToString()
{
return $"Store {StoreId} has been removed";
}
}

View file

@ -0,0 +1,36 @@
#nullable enable
namespace BTCPayServer.Events;
public abstract class StoreRoleEvent(string storeId, string roleId)
{
public string StoreId { get; } = storeId;
public string RoleId { get; } = roleId;
public class Added(string storeId, string roleId) : StoreRoleEvent(storeId, roleId)
{
protected override string ToString()
{
return $"{base.ToString()} has been added";
}
}
public class Removed(string storeId, string roleId) : StoreRoleEvent(storeId, roleId)
{
protected override string ToString()
{
return $"{base.ToString()} has been removed";
}
}
public class Updated(string storeId, string roleId) : StoreRoleEvent(storeId, roleId)
{
protected override string ToString()
{
return $"{base.ToString()} has been updated";
}
}
protected new virtual string ToString()
{
return $"StoreRoleEvent: Store {StoreId}, Role {RoleId}";
}
}

View file

@ -0,0 +1,38 @@
#nullable enable
namespace BTCPayServer.Events;
public abstract class StoreUserEvent(string storeId, string userId)
{
public class Added(string storeId, string userId, string roleId) : StoreUserEvent(storeId, userId)
{
public string RoleId { get; } = roleId;
protected override string ToString()
{
return $"{base.ToString()} has been added";
}
}
public class Removed(string storeId, string userId) : StoreUserEvent(storeId, userId)
{
protected override string ToString()
{
return $"{base.ToString()} has been removed";
}
}
public class Updated(string storeId, string userId, string roleId) : StoreUserEvent(storeId, userId)
{
public string RoleId { get; } = roleId;
protected override string ToString()
{
return $"{base.ToString()} has been updated";
}
}
public string StoreId { get; } = storeId;
public string UserId { get; } = userId;
protected new virtual string ToString()
{
return $"StoreUserEvent: User {UserId}, Store {StoreId}";
}
}

View file

@ -1,10 +0,0 @@
using System;
using BTCPayServer.Data;
namespace BTCPayServer.Events;
public class UserApprovedEvent
{
public ApplicationUser User { get; set; }
public Uri RequestUri { get; set; }
}

View file

@ -1,10 +0,0 @@
using System;
using BTCPayServer.Data;
namespace BTCPayServer.Events;
public class UserConfirmedEmailEvent
{
public ApplicationUser User { get; set; }
public Uri RequestUri { get; set; }
}

View file

@ -0,0 +1,87 @@
#nullable enable
using System;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Services;
using Microsoft.AspNetCore.Http;
namespace BTCPayServer.Events;
public class UserEvent(ApplicationUser user)
{
public class Deleted(ApplicationUser user) : UserEvent(user)
{
protected override string ToString()
{
return $"{base.ToString()} has been deleted";
}
}
public class InviteAccepted(ApplicationUser user, string storeUsersLink) : UserEvent(user)
{
public string StoreUsersLink { get; set; } = storeUsersLink;
}
public class PasswordResetRequested(ApplicationUser user, string resetLink) : UserEvent(user)
{
public string ResetLink { get; } = resetLink;
}
public class Registered(ApplicationUser user, string approvalLink, string confirmationEmail) : UserEvent(user)
{
public string ApprovalLink { get; } = approvalLink;
public string ConfirmationEmailLink { get; set; } = confirmationEmail;
public static async Task<Registered> Create(ApplicationUser user, CallbackGenerator callbackGenerator, HttpRequest request)
{
var approvalLink = callbackGenerator.ForApproval(user, request);
var confirmationEmail = await callbackGenerator.ForEmailConfirmation(user, request);
return new Registered(user, approvalLink, confirmationEmail);
}
}
public class Invited(ApplicationUser user, ApplicationUser invitedBy, string invitationLink, string approvalLink, string confirmationEmail) : Registered(user, approvalLink, confirmationEmail)
{
public bool SendInvitationEmail { get; set; }
public ApplicationUser InvitedByUser { get; } = invitedBy;
public string InvitationLink { get; } = invitationLink;
public static async Task<Invited> Create(ApplicationUser user, ApplicationUser currentUser, CallbackGenerator callbackGenerator, HttpRequest request, bool sendEmail)
{
var invitationLink = await callbackGenerator.ForInvitation(user, request);
var approvalLink = callbackGenerator.ForApproval(user, request);
var confirmationEmail = await callbackGenerator.ForEmailConfirmation(user, request);
return new Invited(user, currentUser, invitationLink, approvalLink, confirmationEmail)
{
SendInvitationEmail = sendEmail
};
}
}
public class Updated(ApplicationUser user) : UserEvent(user)
{
protected override string ToString()
{
return $"{base.ToString()} has been updated";
}
}
public class Approved(ApplicationUser user, string loginLink) : UserEvent(user)
{
public string LoginLink { get; set; } = loginLink;
protected override string ToString()
{
return $"{base.ToString()} has been approved";
}
}
public class ConfirmedEmail(ApplicationUser user, string approvalLink): UserEvent(user)
{
public string ApprovalLink { get; set; } = approvalLink;
protected override string ToString()
{
return $"{base.ToString()} has email confirmed";
}
}
public ApplicationUser User { get; } = user;
protected new virtual string ToString()
{
return $"UserEvent: User \"{User.Email}\" ({User.Id})";
}
}

View file

@ -1,10 +0,0 @@
using System;
using BTCPayServer.Data;
namespace BTCPayServer.Events;
public class UserInviteAcceptedEvent
{
public ApplicationUser User { get; set; }
public Uri RequestUri { get; set; }
}

View file

@ -1,13 +0,0 @@
using System;
using System.Threading.Tasks;
using BTCPayServer.Data;
namespace BTCPayServer.Events
{
public class UserPasswordResetRequestedEvent
{
public ApplicationUser User { get; set; }
public Uri RequestUri { get; set; }
public TaskCompletionSource<Uri> CallbackUrlGenerated;
}
}

View file

@ -1,22 +0,0 @@
using System;
using System.Threading.Tasks;
using BTCPayServer.Data;
namespace BTCPayServer.Events;
public class UserRegisteredEvent
{
public ApplicationUser User { get; set; }
public bool Admin { get; set; }
public UserRegisteredEventKind Kind { get; set; } = UserRegisteredEventKind.Registration;
public Uri RequestUri { get; set; }
public ApplicationUser InvitedByUser { get; set; }
public bool SendInvitationEmail { get; set; } = true;
public TaskCompletionSource<Uri> CallbackUrlGenerated;
}
public enum UserRegisteredEventKind
{
Registration,
Invite
}

View file

@ -23,52 +23,11 @@ namespace Microsoft.AspNetCore.Mvc
} }
#nullable restore #nullable restore
public static string UserDetailsLink(this LinkGenerator urlHelper, string userId, string scheme, HostString host, string pathbase)
{
return urlHelper.GetUriByAction(nameof(UIServerController.User), "UIServer",
new { userId }, scheme, host, pathbase);
}
public static string StoreUsersLink(this LinkGenerator urlHelper, string storeId, string scheme, HostString host, string pathbase)
{
return urlHelper.GetUriByAction(nameof(UIStoresController.StoreUsers), "UIStores",
new { storeId }, scheme, host, pathbase);
}
public static string InvitationLink(this LinkGenerator urlHelper, string userId, string code, string scheme, HostString host, string pathbase)
{
return urlHelper.GetUriByAction(nameof(UIAccountController.AcceptInvite), "UIAccount",
new { userId, code }, scheme, host, pathbase);
}
public static string EmailConfirmationLink(this LinkGenerator urlHelper, string userId, string code, string scheme, HostString host, string pathbase)
{
return urlHelper.GetUriByAction(nameof(UIAccountController.ConfirmEmail), "UIAccount",
new { userId, code }, scheme, host, pathbase);
}
public static string LoginLink(this LinkGenerator urlHelper, string scheme, HostString host, string pathbase)
{
return urlHelper.GetUriByAction(nameof(UIAccountController.Login), "UIAccount", null , scheme, host, pathbase);
}
public static string LoginCodeLink(this LinkGenerator urlHelper, string loginCode, string returnUrl, string scheme, HostString host, string pathbase) public static string LoginCodeLink(this LinkGenerator urlHelper, string loginCode, string returnUrl, string scheme, HostString host, string pathbase)
{ {
return urlHelper.GetUriByAction(nameof(UIAccountController.LoginUsingCode), "UIAccount", new { loginCode, returnUrl }, scheme, host, pathbase); return urlHelper.GetUriByAction(nameof(UIAccountController.LoginUsingCode), "UIAccount", new { loginCode, returnUrl }, scheme, host, pathbase);
} }
public static string ResetPasswordLink(this LinkGenerator urlHelper, string userId, string code, string scheme, HostString host, string pathbase)
{
return urlHelper.GetUriByAction(
action: nameof(UIAccountController.SetPassword),
controller: "UIAccount",
values: new { userId, code },
scheme: scheme,
host: host,
pathBase: pathbase
);
}
public static string PaymentRequestLink(this LinkGenerator urlHelper, string paymentRequestId, string scheme, HostString host, string pathbase) public static string PaymentRequestLink(this LinkGenerator urlHelper, string paymentRequestId, string scheme, HostString host, string pathbase)
{ {
return urlHelper.GetUriByAction( return urlHelper.GetUriByAction(

View file

@ -1,4 +1,3 @@
using System;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Data; using BTCPayServer.Data;
@ -9,10 +8,7 @@ using BTCPayServer.Services.Mails;
using BTCPayServer.Services.Notifications; using BTCPayServer.Services.Notifications;
using BTCPayServer.Services.Notifications.Blobs; using BTCPayServer.Services.Notifications.Blobs;
using BTCPayServer.Services.Stores; using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace BTCPayServer.HostedServices; namespace BTCPayServer.HostedServices;
@ -20,41 +16,41 @@ namespace BTCPayServer.HostedServices;
public class UserEventHostedService( public class UserEventHostedService(
EventAggregator eventAggregator, EventAggregator eventAggregator,
UserManager<ApplicationUser> userManager, UserManager<ApplicationUser> userManager,
CallbackGenerator callbackGenerator,
EmailSenderFactory emailSenderFactory, EmailSenderFactory emailSenderFactory,
NotificationSender notificationSender, NotificationSender notificationSender,
StoreRepository storeRepository, StoreRepository storeRepository,
LinkGenerator generator,
Logs logs) Logs logs)
: EventHostedServiceBase(eventAggregator, logs) : EventHostedServiceBase(eventAggregator, logs)
{ {
public UserManager<ApplicationUser> UserManager { get; } = userManager;
public CallbackGenerator CallbackGenerator { get; } = callbackGenerator;
protected override void SubscribeToEvents() protected override void SubscribeToEvents()
{ {
Subscribe<UserRegisteredEvent>(); Subscribe<UserEvent.Registered>();
Subscribe<UserApprovedEvent>(); Subscribe<UserEvent.Invited>();
Subscribe<UserConfirmedEmailEvent>(); Subscribe<UserEvent.Approved>();
Subscribe<UserPasswordResetRequestedEvent>(); Subscribe<UserEvent.ConfirmedEmail>();
Subscribe<UserInviteAcceptedEvent>(); Subscribe<UserEvent.PasswordResetRequested>();
Subscribe<UserEvent.InviteAccepted>();
} }
protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken) protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken)
{ {
string code; ApplicationUser user = (evt as UserEvent).User;
string callbackUrl;
Uri uri;
HostString host;
ApplicationUser user;
IEmailSender emailSender; IEmailSender emailSender;
switch (evt) switch (evt)
{ {
case UserRegisteredEvent ev: case UserEvent.Registered ev:
user = ev.User;
uri = ev.RequestUri;
host = new HostString(uri.Host, uri.Port);
// can be either a self-registration or by invite from another user // can be either a self-registration or by invite from another user
var isInvite = ev.Kind == UserRegisteredEventKind.Invite; var type = await UserManager.IsInRoleAsync(user, Roles.ServerAdmin) ? "admin" : "user";
var type = ev.Admin ? "admin" : "user"; var info = ev switch
var info = isInvite ? ev.InvitedByUser != null ? $"invited by {ev.InvitedByUser.Email}" : "invited" : "registered"; {
UserEvent.Invited { InvitedByUser: { } invitedBy } => $"invited by {invitedBy.Email}",
UserEvent.Invited => "invited",
_ => "registered"
};
var requiresApproval = user.RequiresApproval && !user.Approved; var requiresApproval = user.RequiresApproval && !user.Approved;
var requiresEmailConfirmation = user.RequiresEmailConfirmation && !user.EmailConfirmed; var requiresEmailConfirmation = user.RequiresEmailConfirmation && !user.EmailConfirmed;
@ -66,84 +62,55 @@ public class UserEventHostedService(
// inform admins only about qualified users and not annoy them with bot registrations. // inform admins only about qualified users and not annoy them with bot registrations.
if (requiresApproval && !requiresEmailConfirmation) if (requiresApproval && !requiresEmailConfirmation)
{ {
await NotifyAdminsAboutUserRequiringApproval(user, uri, newUserInfo); await NotifyAdminsAboutUserRequiringApproval(user, ev.ApprovalLink, newUserInfo);
} }
// set callback result and send email to user // set callback result and send email to user
emailSender = await emailSenderFactory.GetEmailSender(); emailSender = await emailSenderFactory.GetEmailSender();
if (isInvite) if (ev is UserEvent.Invited invited)
{ {
code = await userManager.GenerateInvitationTokenAsync<ApplicationUser>(user.Id); if (invited.SendInvitationEmail)
callbackUrl = generator.InvitationLink(user.Id, code, uri.Scheme, host, uri.PathAndQuery); emailSender.SendInvitation(user.GetMailboxAddress(), invited.InvitationLink);
ev.CallbackUrlGenerated?.SetResult(new Uri(callbackUrl));
if (ev.SendInvitationEmail)
emailSender.SendInvitation(user.GetMailboxAddress(), callbackUrl);
} }
else if (requiresEmailConfirmation) else if (requiresEmailConfirmation)
{ {
code = await userManager.GenerateEmailConfirmationTokenAsync(user); emailSender.SendEmailConfirmation(user.GetMailboxAddress(), ev.ConfirmationEmailLink);
callbackUrl = generator.EmailConfirmationLink(user.Id, code, uri.Scheme, host, uri.PathAndQuery);
ev.CallbackUrlGenerated?.SetResult(new Uri(callbackUrl));
emailSender.SendEmailConfirmation(user.GetMailboxAddress(), callbackUrl);
}
else
{
ev.CallbackUrlGenerated?.SetResult(null);
} }
break; break;
case UserPasswordResetRequestedEvent pwResetEvent: case UserEvent.PasswordResetRequested pwResetEvent:
user = pwResetEvent.User;
uri = pwResetEvent.RequestUri;
host = new HostString(uri.Host, uri.Port);
code = await userManager.GeneratePasswordResetTokenAsync(user);
callbackUrl = generator.ResetPasswordLink(user.Id, code, uri.Scheme, host, uri.PathAndQuery);
pwResetEvent.CallbackUrlGenerated?.SetResult(new Uri(callbackUrl));
Logs.PayServer.LogInformation("User {Email} requested a password reset", user.Email); Logs.PayServer.LogInformation("User {Email} requested a password reset", user.Email);
emailSender = await emailSenderFactory.GetEmailSender(); emailSender = await emailSenderFactory.GetEmailSender();
emailSender.SendResetPassword(user.GetMailboxAddress(), callbackUrl); emailSender.SendResetPassword(user.GetMailboxAddress(), pwResetEvent.ResetLink);
break; break;
case UserApprovedEvent approvedEvent: case UserEvent.Approved approvedEvent:
user = approvedEvent.User;
if (!user.Approved) break; if (!user.Approved) break;
uri = approvedEvent.RequestUri;
host = new HostString(uri.Host, uri.Port);
callbackUrl = generator.LoginLink(uri.Scheme, host, uri.PathAndQuery);
emailSender = await emailSenderFactory.GetEmailSender(); emailSender = await emailSenderFactory.GetEmailSender();
emailSender.SendApprovalConfirmation(user.GetMailboxAddress(), callbackUrl); emailSender.SendApprovalConfirmation(user.GetMailboxAddress(), approvedEvent.LoginLink);
break; break;
case UserConfirmedEmailEvent confirmedEvent: case UserEvent.ConfirmedEmail confirmedEvent:
user = confirmedEvent.User;
if (!user.EmailConfirmed) break; if (!user.EmailConfirmed) break;
uri = confirmedEvent.RequestUri;
var confirmedUserInfo = $"User {user.Email} confirmed their email address"; var confirmedUserInfo = $"User {user.Email} confirmed their email address";
Logs.PayServer.LogInformation(confirmedUserInfo); Logs.PayServer.LogInformation(confirmedUserInfo);
if (!user.RequiresApproval || user.Approved) return; await NotifyAdminsAboutUserRequiringApproval(user, confirmedEvent.ApprovalLink, confirmedUserInfo);
await NotifyAdminsAboutUserRequiringApproval(user, uri, confirmedUserInfo);
break; break;
case UserInviteAcceptedEvent inviteAcceptedEvent: case UserEvent.InviteAccepted inviteAcceptedEvent:
user = inviteAcceptedEvent.User;
uri = inviteAcceptedEvent.RequestUri;
Logs.PayServer.LogInformation("User {Email} accepted the invite", user.Email); Logs.PayServer.LogInformation("User {Email} accepted the invite", user.Email);
await NotifyAboutUserAcceptingInvite(user, uri); await NotifyAboutUserAcceptingInvite(user, inviteAcceptedEvent.StoreUsersLink);
break; break;
} }
} }
private async Task NotifyAdminsAboutUserRequiringApproval(ApplicationUser user, Uri uri, string newUserInfo) private async Task NotifyAdminsAboutUserRequiringApproval(ApplicationUser user, string approvalLink, string newUserInfo)
{ {
if (!user.RequiresApproval || user.Approved) return; if (!user.RequiresApproval || user.Approved) return;
// notification // notification
await notificationSender.SendNotification(new AdminScope(), new NewUserRequiresApprovalNotification(user)); await notificationSender.SendNotification(new AdminScope(), new NewUserRequiresApprovalNotification(user));
// email // email
var admins = await userManager.GetUsersInRoleAsync(Roles.ServerAdmin); var admins = await UserManager.GetUsersInRoleAsync(Roles.ServerAdmin);
var host = new HostString(uri.Host, uri.Port);
var approvalLink = generator.UserDetailsLink(user.Id, uri.Scheme, host, uri.PathAndQuery);
var emailSender = await emailSenderFactory.GetEmailSender(); var emailSender = await emailSenderFactory.GetEmailSender();
foreach (var admin in admins) foreach (var admin in admins)
{ {
@ -151,7 +118,7 @@ public class UserEventHostedService(
} }
} }
private async Task NotifyAboutUserAcceptingInvite(ApplicationUser user, Uri uri) private async Task NotifyAboutUserAcceptingInvite(ApplicationUser user, string storeUsersLink)
{ {
var stores = await storeRepository.GetStoresByUserId(user.Id); var stores = await storeRepository.GetStoresByUserId(user.Id);
var notifyRoles = new[] { StoreRoleId.Owner, StoreRoleId.Manager }; var notifyRoles = new[] { StoreRoleId.Owner, StoreRoleId.Manager };
@ -161,15 +128,14 @@ public class UserEventHostedService(
await notificationSender.SendNotification(new StoreScope(store.Id, notifyRoles), new InviteAcceptedNotification(user, store)); await notificationSender.SendNotification(new StoreScope(store.Id, notifyRoles), new InviteAcceptedNotification(user, store));
// email // email
var notifyUsers = await storeRepository.GetStoreUsers(store.Id, notifyRoles); var notifyUsers = await storeRepository.GetStoreUsers(store.Id, notifyRoles);
var host = new HostString(uri.Host, uri.Port); var link = string.Format(storeUsersLink, store.Id);
var storeUsersLink = generator.StoreUsersLink(store.Id, uri.Scheme, host, uri.PathAndQuery);
var emailSender = await emailSenderFactory.GetEmailSender(store.Id); var emailSender = await emailSenderFactory.GetEmailSender(store.Id);
foreach (var storeUser in notifyUsers) foreach (var storeUser in notifyUsers)
{ {
if (storeUser.Id == user.Id) continue; // do not notify the user itself (if they were added as owner or manager) if (storeUser.Id == user.Id) continue; // do not notify the user itself (if they were added as owner or manager)
var notifyUser = await userManager.FindByIdOrEmail(storeUser.Id); var notifyUser = await UserManager.FindByIdOrEmail(storeUser.Id);
var info = $"User {user.Email} accepted the invite to {store.StoreName}"; var info = $"User {user.Email} accepted the invite to {store.StoreName}";
emailSender.SendUserInviteAcceptedInfo(notifyUser.GetMailboxAddress(), info, storeUsersLink); emailSender.SendUserInviteAcceptedInfo(notifyUser.GetMailboxAddress(), info, link);
} }
} }
} }

View file

@ -86,6 +86,7 @@ namespace BTCPayServer.Hosting
} }
public static IServiceCollection AddBTCPayServer(this IServiceCollection services, IConfiguration configuration, Logs logs) public static IServiceCollection AddBTCPayServer(this IServiceCollection services, IConfiguration configuration, Logs logs)
{ {
services.TryAddSingleton<CallbackGenerator>();
services.TryAddSingleton<IStringLocalizerFactory, LocalizerFactory>(); services.TryAddSingleton<IStringLocalizerFactory, LocalizerFactory>();
services.TryAddSingleton<IHtmlLocalizerFactory, LocalizerFactory>(); services.TryAddSingleton<IHtmlLocalizerFactory, LocalizerFactory>();
services.TryAddSingleton<LocalizerService>(); services.TryAddSingleton<LocalizerService>();

View file

@ -154,7 +154,7 @@ namespace BTCPayServer.Payments.Bitcoin
{ {
case NBXplorer.Models.NewBlockEvent evt: case NBXplorer.Models.NewBlockEvent evt:
await UpdatePaymentStates(wallet); await UpdatePaymentStates(wallet);
_Aggregator.Publish(new Events.NewBlockEvent() { PaymentMethodId = pmi }); _Aggregator.Publish(new Events.NewBlockEvent() { PaymentMethodId = pmi, AdditionalInfo = evt });
break; break;
case NBXplorer.Models.NewTransactionEvent evt: case NBXplorer.Models.NewTransactionEvent evt:
if (evt.DerivationStrategy != null) if (evt.DerivationStrategy != null)

View file

@ -3,11 +3,9 @@ using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using System.Threading.Channels; using System.Threading.Channels;
using System.Threading.Tasks; using System.Threading.Tasks;
using Amazon.Runtime.Internal;
using BTCPayServer.Client.Models; using BTCPayServer.Client.Models;
using BTCPayServer.Configuration; using BTCPayServer.Configuration;
using BTCPayServer.Data; using BTCPayServer.Data;
@ -18,13 +16,11 @@ using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Services; using BTCPayServer.Services;
using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Stores; using BTCPayServer.Services.Stores;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using NBitcoin; using NBitcoin;
using NBitpayClient;
using NBXplorer; using NBXplorer;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
@ -263,15 +259,24 @@ retry:
{ {
lock (_InstanceListeners) lock (_InstanceListeners)
{ {
foreach ((_, var instance) in _InstanceListeners.ToArray()) foreach (var key in _InstanceListeners.Keys)
{ {
instance.RemoveExpiredInvoices(); CheckConnection(key.Item1, key.Item2);
if (!instance.Empty)
instance.EnsureListening(_Cts.Token);
} }
} }
} }
public void CheckConnection(string cryptoCode, string connStr)
{
if (_InstanceListeners.TryGetValue((cryptoCode, connStr), out var instance))
{
instance.RemoveExpiredInvoices();
if (!instance.Empty)
instance.EnsureListening(_Cts.Token);
}
}
private async Task CreateNewLNInvoiceForBTCPayInvoice(InvoiceEntity invoice) private async Task CreateNewLNInvoiceForBTCPayInvoice(InvoiceEntity invoice)
{ {
var paymentMethods = GetLightningPrompts(invoice).ToArray(); var paymentMethods = GetLightningPrompts(invoice).ToArray();

View file

@ -6,7 +6,9 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Client; using BTCPayServer.Client;
using BTCPayServer.Client.Models; using BTCPayServer.Client.Models;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Models.AppViewModels; using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Plugins.Crowdfund; using BTCPayServer.Plugins.Crowdfund;
using BTCPayServer.Plugins.PointOfSale; using BTCPayServer.Plugins.PointOfSale;
@ -28,7 +30,7 @@ namespace BTCPayServer.Services.Apps
private readonly Dictionary<string, AppBaseType> _appTypes; private readonly Dictionary<string, AppBaseType> _appTypes;
static AppService() static AppService()
{ {
_defaultSerializer = new JsonSerializerSettings() _defaultSerializer = new JsonSerializerSettings
{ {
ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver(), ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver(),
Formatting = Formatting.None Formatting = Formatting.None
@ -40,8 +42,8 @@ namespace BTCPayServer.Services.Apps
readonly ApplicationDbContextFactory _ContextFactory; readonly ApplicationDbContextFactory _ContextFactory;
private readonly InvoiceRepository _InvoiceRepository; private readonly InvoiceRepository _InvoiceRepository;
readonly CurrencyNameTable _Currencies; readonly CurrencyNameTable _Currencies;
private readonly DisplayFormatter _displayFormatter;
private readonly StoreRepository _storeRepository; private readonly StoreRepository _storeRepository;
private readonly EventAggregator _eventAggregator;
public CurrencyNameTable Currencies => _Currencies; public CurrencyNameTable Currencies => _Currencies;
private readonly string[] _paidStatuses = [ private readonly string[] _paidStatuses = [
InvoiceStatus.Processing.ToString(), InvoiceStatus.Processing.ToString(),
@ -53,15 +55,15 @@ namespace BTCPayServer.Services.Apps
ApplicationDbContextFactory contextFactory, ApplicationDbContextFactory contextFactory,
InvoiceRepository invoiceRepository, InvoiceRepository invoiceRepository,
CurrencyNameTable currencies, CurrencyNameTable currencies,
DisplayFormatter displayFormatter, StoreRepository storeRepository,
StoreRepository storeRepository) EventAggregator eventAggregator)
{ {
_appTypes = apps.ToDictionary(a => a.Type, a => a); _appTypes = apps.ToDictionary(a => a.Type, a => a);
_ContextFactory = contextFactory; _ContextFactory = contextFactory;
_InvoiceRepository = invoiceRepository; _InvoiceRepository = invoiceRepository;
_Currencies = currencies; _Currencies = currencies;
_storeRepository = storeRepository; _storeRepository = storeRepository;
_displayFormatter = displayFormatter; _eventAggregator = eventAggregator;
} }
#nullable enable #nullable enable
public Dictionary<string, string> GetAvailableAppTypes() public Dictionary<string, string> GetAvailableAppTypes()
@ -231,6 +233,7 @@ namespace BTCPayServer.Services.Apps
await using var ctx = _ContextFactory.CreateContext(); await using var ctx = _ContextFactory.CreateContext();
ctx.Apps.Add(appData); ctx.Apps.Add(appData);
ctx.Entry(appData).State = EntityState.Deleted; ctx.Entry(appData).State = EntityState.Deleted;
_eventAggregator.Publish(new AppEvent.Deleted(appData));
return await ctx.SaveChangesAsync() == 1; return await ctx.SaveChangesAsync() == 1;
} }
@ -239,6 +242,7 @@ namespace BTCPayServer.Services.Apps
await using var ctx = _ContextFactory.CreateContext(); await using var ctx = _ContextFactory.CreateContext();
appData.Archived = archived; appData.Archived = archived;
ctx.Entry(appData).State = EntityState.Modified; ctx.Entry(appData).State = EntityState.Modified;
_eventAggregator.Publish(new AppEvent.Updated(appData));
return await ctx.SaveChangesAsync() == 1; return await ctx.SaveChangesAsync() == 1;
} }
@ -446,7 +450,8 @@ retry:
public async Task UpdateOrCreateApp(AppData app) public async Task UpdateOrCreateApp(AppData app)
{ {
await using var ctx = _ContextFactory.CreateContext(); await using var ctx = _ContextFactory.CreateContext();
if (string.IsNullOrEmpty(app.Id)) var newApp = string.IsNullOrEmpty(app.Id);
if (newApp)
{ {
app.Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(20)); app.Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(20));
app.Created = DateTimeOffset.UtcNow; app.Created = DateTimeOffset.UtcNow;
@ -460,6 +465,10 @@ retry:
ctx.Entry(app).Property(data => data.AppType).IsModified = false; ctx.Entry(app).Property(data => data.AppType).IsModified = false;
} }
await ctx.SaveChangesAsync(); await ctx.SaveChangesAsync();
if (newApp)
_eventAggregator.Publish(new AppEvent.Created(app));
else
_eventAggregator.Publish(new AppEvent.Updated(app));
} }
private static bool TryParseJson(string json, [MaybeNullWhen(false)] out JObject result) private static bool TryParseJson(string json, [MaybeNullWhen(false)] out JObject result)

View file

@ -0,0 +1,84 @@
#nullable enable
using System;
using System.Reflection.Emit;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Abstractions.Extensions;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Hosting;
using Microsoft.AspNetCore.Mvc;
using NBitcoin.Altcoins.ArgoneumInternals;
using BTCPayServer.Controllers;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Http.Extensions;
using NBitcoin.DataEncoders;
using System.Runtime.CompilerServices;
namespace BTCPayServer.Services
{
public class CallbackGenerator(LinkGenerator linkGenerator, UserManager<ApplicationUser> userManager)
{
public LinkGenerator LinkGenerator { get; } = linkGenerator;
public UserManager<ApplicationUser> UserManager { get; } = userManager;
public string ForLNUrlAuth(ApplicationUser user, byte[] r, HttpRequest request)
{
return LinkGenerator.GetUriByAction(
action: nameof(UILNURLAuthController.LoginResponse),
controller: "UILNURLAuth",
values: new { userId = user.Id, action = "login", tag = "login", k1 = Encoders.Hex.EncodeData(r) },
request.Scheme,
request.Host,
request.PathBase) ?? throw Bug();
}
public string StoreUsersLink(string storeId, HttpRequest request)
{
return LinkGenerator.GetUriByAction(nameof(UIStoresController.StoreUsers), "UIStores",
new { storeId }, request.Scheme, request.Host, request.PathBase) ?? throw Bug();
}
public async Task<string> ForEmailConfirmation(ApplicationUser user, HttpRequest request)
{
var code = await UserManager.GenerateEmailConfirmationTokenAsync(user);
return LinkGenerator.GetUriByAction(nameof(UIAccountController.ConfirmEmail), "UIAccount",
new { userId = user.Id, code }, request.Scheme, request.Host, request.PathBase) ?? throw Bug();
}
public async Task<string> ForInvitation(ApplicationUser user, HttpRequest request)
{
var code = await UserManager.GenerateInvitationTokenAsync<ApplicationUser>(user.Id) ?? throw Bug();
return ForInvitation(user, code, request);
}
public string ForInvitation(ApplicationUser user, string code, HttpRequest request)
{
return LinkGenerator.GetUriByAction(nameof(UIAccountController.AcceptInvite), "UIAccount",
new { userId = user.Id, code }, request.Scheme, request.Host, request.PathBase) ?? throw Bug();
}
public async Task<string> ForPasswordReset(ApplicationUser user, HttpRequest request)
{
var code = await UserManager.GeneratePasswordResetTokenAsync(user);
return LinkGenerator.GetUriByAction(
action: nameof(UIAccountController.SetPassword),
controller: "UIAccount",
values: new { userId = user.Id, code },
scheme: request.Scheme,
host: request.Host,
pathBase: request.PathBase
) ?? throw Bug();
}
public string ForApproval(ApplicationUser user, HttpRequest request)
{
return LinkGenerator.GetUriByAction(nameof(UIServerController.User), "UIServer",
new { userId = user.Id }, request.Scheme, request.Host, request.PathBase) ?? throw Bug();
}
public string ForLogin(ApplicationUser user, HttpRequest request)
{
return LinkGenerator.GetUriByAction(nameof(UIAccountController.Login), "UIAccount", new { email = user.Email }, request.Scheme, request.Host, request.PathBase) ?? throw Bug();
}
private Exception Bug([CallerMemberName] string? name = null) => new InvalidOperationException($"Error generating link for {name} (Report this bug to BTCPay Server github repository)");
}
}

View file

@ -2,19 +2,16 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection.Metadata;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Abstractions.Contracts; using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Client; using BTCPayServer.Client;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Events; using BTCPayServer.Events;
using BTCPayServer.Migrations;
using Dapper; using Dapper;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using NBitcoin; using NBitcoin;
using NBitcoin.DataEncoders; using NBitcoin.DataEncoders;
using Newtonsoft.Json; using Newtonsoft.Json;
using static BTCPayServer.Services.Stores.StoreRepository;
namespace BTCPayServer.Services.Stores namespace BTCPayServer.Services.Stores
{ {
@ -157,6 +154,7 @@ namespace BTCPayServer.Services.Stores
return "This is the last role that allows to modify store settings, you cannot remove it"; return "This is the last role that allows to modify store settings, you cannot remove it";
ctx.StoreRoles.Remove(match); ctx.StoreRoles.Remove(match);
await ctx.SaveChangesAsync(); await ctx.SaveChangesAsync();
_eventAggregator.Publish(new StoreRoleEvent.Removed(role.StoreId!, role.Id));
return null; return null;
} }
@ -168,15 +166,21 @@ namespace BTCPayServer.Services.Stores
policies = policies.Where(s => Policies.IsValidPolicy(s) && Policies.IsStorePolicy(s)).ToList(); policies = policies.Where(s => Policies.IsValidPolicy(s) && Policies.IsStorePolicy(s)).ToList();
await using var ctx = _ContextFactory.CreateContext(); await using var ctx = _ContextFactory.CreateContext();
Data.StoreRole? match = await ctx.StoreRoles.FindAsync(role.Id); Data.StoreRole? match = await ctx.StoreRoles.FindAsync(role.Id);
var added = false;
if (match is null) if (match is null)
{ {
match = new Data.StoreRole() { Id = role.Id, StoreDataId = role.StoreId, Role = role.Role }; match = new Data.StoreRole { Id = role.Id, StoreDataId = role.StoreId, Role = role.Role };
ctx.StoreRoles.Add(match); ctx.StoreRoles.Add(match);
added = true;
} }
match.Permissions = policies; match.Permissions = policies;
try try
{ {
await ctx.SaveChangesAsync(); await ctx.SaveChangesAsync();
StoreRoleEvent evt = added
? new StoreRoleEvent.Added(role.StoreId!, role.Id)
: new StoreRoleEvent.Updated(role.StoreId!, role.Id);
_eventAggregator.Publish(evt);
} }
catch (DbUpdateException) catch (DbUpdateException)
{ {
@ -301,6 +305,7 @@ namespace BTCPayServer.Services.Stores
try try
{ {
await ctx.SaveChangesAsync(); await ctx.SaveChangesAsync();
_eventAggregator.Publish(new StoreUserEvent.Added(storeId, userId, roleId.Id));
return true; return true;
} }
catch (DbUpdateException) catch (DbUpdateException)
@ -316,10 +321,12 @@ namespace BTCPayServer.Services.Stores
roleId ??= await GetDefaultRole(); roleId ??= await GetDefaultRole();
await using var ctx = _ContextFactory.CreateContext(); await using var ctx = _ContextFactory.CreateContext();
var userStore = await ctx.UserStore.FindAsync(userId, storeId); var userStore = await ctx.UserStore.FindAsync(userId, storeId);
var added = false;
if (userStore is null) if (userStore is null)
{ {
userStore = new UserStore { StoreDataId = storeId, ApplicationUserId = userId }; userStore = new UserStore { StoreDataId = storeId, ApplicationUserId = userId };
ctx.UserStore.Add(userStore); ctx.UserStore.Add(userStore);
added = true;
} }
if (userStore.StoreRoleId == roleId.Id) if (userStore.StoreRoleId == roleId.Id)
@ -329,6 +336,10 @@ namespace BTCPayServer.Services.Stores
try try
{ {
await ctx.SaveChangesAsync(); await ctx.SaveChangesAsync();
StoreUserEvent evt = added
? new StoreUserEvent.Added(storeId, userId, userStore.StoreRoleId)
: new StoreUserEvent.Updated(storeId, userId, userStore.StoreRoleId);
_eventAggregator.Publish(evt);
return true; return true;
} }
catch (DbUpdateException) catch (DbUpdateException)
@ -343,22 +354,6 @@ namespace BTCPayServer.Services.Stores
throw new ArgumentException("The roleId doesn't belong to this storeId", nameof(roleId)); throw new ArgumentException("The roleId doesn't belong to this storeId", nameof(roleId));
} }
public async Task CleanUnreachableStores()
{
await using var ctx = _ContextFactory.CreateContext();
var events = new List<Events.StoreRemovedEvent>();
foreach (var store in await ctx.Stores.Include(data => data.UserStores)
.ThenInclude(store => store.StoreRole).Where(s =>
s.UserStores.All(u => !u.StoreRole.Permissions.Contains(Policies.CanModifyStoreSettings)))
.ToArrayAsync())
{
ctx.Stores.Remove(store);
events.Add(new Events.StoreRemovedEvent(store.Id));
}
await ctx.SaveChangesAsync();
events.ForEach(e => _eventAggregator.Publish(e));
}
public async Task<bool> RemoveStoreUser(string storeId, string userId) public async Task<bool> RemoveStoreUser(string storeId, string userId)
{ {
await using var ctx = _ContextFactory.CreateContext(); await using var ctx = _ContextFactory.CreateContext();
@ -370,8 +365,8 @@ namespace BTCPayServer.Services.Stores
ctx.UserStore.Add(userStore); ctx.UserStore.Add(userStore);
ctx.Entry(userStore).State = EntityState.Deleted; ctx.Entry(userStore).State = EntityState.Deleted;
await ctx.SaveChangesAsync(); await ctx.SaveChangesAsync();
_eventAggregator.Publish(new StoreUserEvent.Removed(storeId, userId));
return true; return true;
} }
private async Task DeleteStoreIfOrphan(string storeId) private async Task DeleteStoreIfOrphan(string storeId)
@ -384,7 +379,7 @@ namespace BTCPayServer.Services.Stores
{ {
ctx.Stores.Remove(store); ctx.Stores.Remove(store);
await ctx.SaveChangesAsync(); await ctx.SaveChangesAsync();
_eventAggregator.Publish(new StoreRemovedEvent(store.Id)); _eventAggregator.Publish(new StoreEvent.Removed(store));
} }
} }
} }
@ -410,6 +405,8 @@ namespace BTCPayServer.Services.Stores
ctx.Add(storeData); ctx.Add(storeData);
ctx.Add(userStore); ctx.Add(userStore);
await ctx.SaveChangesAsync(); await ctx.SaveChangesAsync();
_eventAggregator.Publish(new StoreUserEvent.Added(storeData.Id, userStore.ApplicationUserId, roleId.Id));
_eventAggregator.Publish(new StoreEvent.Created(storeData));
} }
public async Task<WebhookData[]> GetWebhooks(string storeId) public async Task<WebhookData[]> GetWebhooks(string storeId)
@ -558,6 +555,7 @@ namespace BTCPayServer.Services.Stores
{ {
ctx.Entry(existing).CurrentValues.SetValues(store); ctx.Entry(existing).CurrentValues.SetValues(store);
await ctx.SaveChangesAsync().ConfigureAwait(false); await ctx.SaveChangesAsync().ConfigureAwait(false);
_eventAggregator.Publish(new StoreEvent.Updated(store));
} }
} }
@ -579,6 +577,8 @@ retry:
try try
{ {
await ctx.SaveChangesAsync(); await ctx.SaveChangesAsync();
if (store != null)
_eventAggregator.Publish(new StoreEvent.Removed(store));
} }
catch (DbUpdateException ex) when (IsDeadlock(ex) && retry < 5) catch (DbUpdateException ex) when (IsDeadlock(ex) && retry < 5)
{ {

View file

@ -108,7 +108,7 @@ namespace BTCPayServer.Services
return true; return true;
} }
public async Task<bool> SetUserApproval(string userId, bool approved, Uri requestUri) public async Task<bool> SetUserApproval(string userId, bool approved, string loginLink)
{ {
using var scope = _serviceProvider.CreateScope(); using var scope = _serviceProvider.CreateScope();
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>(); var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
@ -123,7 +123,7 @@ namespace BTCPayServer.Services
if (succeeded) if (succeeded)
{ {
_logger.LogInformation("User {Email} is now {Status}", user.Email, approved ? "approved" : "unapproved"); _logger.LogInformation("User {Email} is now {Status}", user.Email, approved ? "approved" : "unapproved");
_eventAggregator.Publish(new UserApprovedEvent { User = user, RequestUri = requestUri }); _eventAggregator.Publish(new UserEvent.Approved(user, loginLink));
} }
else else
{ {