diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index ce12b8e57..dcdb05514 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -3829,7 +3829,6 @@ namespace BTCPayServer.Tests await tester.WaitForEvent(async () => { - await tester.ExplorerNode.GenerateAsync(1); }, bevent => bevent.PaymentMethodId == PaymentTypes.CHAIN.GetPaymentMethodId("BTC")); diff --git a/BTCPayServer/App/API/AppApiController.Account.cs b/BTCPayServer/App/API/AppApiController.Account.cs index 6aa2ea3b1..ca40856dc 100644 --- a/BTCPayServer/App/API/AppApiController.Account.cs +++ b/BTCPayServer/App/API/AppApiController.Account.cs @@ -70,12 +70,7 @@ public partial class AppApiController await settingsRepository.FirstAdminRegistered(policies, btcpayOptions.UpdateUrl != null, btcpayOptions.DisableRegistration, logs); } - eventAggregator.Publish(new UserRegisteredEvent - { - RequestUri = Request.GetAbsoluteRootUri(), - User = user, - Admin = isFirstAdmin - }); + eventAggregator.Publish(await UserEvent.Registered.Create(user, callbackGenerator, Request)); SignInResult? signInResult = null; var requiresApproval = policies.RequiresUserApproval && !user.Approved; @@ -224,12 +219,10 @@ public partial class AppApiController bool? emailHasBeenConfirmed = requiresEmailConfirmation ? false : null; var requiresSetPassword = !await userManager.HasPasswordAsync(user); string? passwordSetCode = requiresSetPassword ? await userManager.GeneratePasswordResetTokenAsync(user) : null; - - eventAggregator.Publish(new UserInviteAcceptedEvent - { - User = user, - RequestUri = Request.GetAbsoluteRootUri() - }); + + // This is a placeholder, the real storeIds will be set by the UserEventHostedService + var storeUsersLink = callbackGenerator.StoreUsersLink("{0}", Request); + eventAggregator.Publish(new UserEvent.InviteAccepted(user, storeUsersLink)); if (requiresEmailConfirmation) { @@ -238,11 +231,8 @@ public partial class AppApiController if (result.Succeeded) { emailHasBeenConfirmed = true; - eventAggregator.Publish(new UserConfirmedEmailEvent - { - User = user, - RequestUri = Request.GetAbsoluteRootUri() - }); + var approvalLink = callbackGenerator.ForApproval(user, Request); + eventAggregator.Publish(new UserEvent.ConfirmedEmail(user, approvalLink)); } } @@ -321,11 +311,8 @@ public partial class AppApiController var user = await userManager.FindByEmailAsync(resetRequest.Email); if (UserService.TryCanLogin(user, out _)) { - eventAggregator.Publish(new UserPasswordResetRequestedEvent - { - User = user, - RequestUri = Request.GetAbsoluteRootUri() - }); + var callbackUri = await callbackGenerator.ForPasswordReset(user, Request); + eventAggregator.Publish(new UserEvent.PasswordResetRequested(user, callbackUri)); } return TypedResults.Ok(); } diff --git a/BTCPayServer/App/API/AppApiController.cs b/BTCPayServer/App/API/AppApiController.cs index 37a2f0fa7..2fc992851 100644 --- a/BTCPayServer/App/API/AppApiController.cs +++ b/BTCPayServer/App/API/AppApiController.cs @@ -30,6 +30,7 @@ public partial class AppApiController( StoreRepository storeRepository, AppService appService, EventAggregator eventAggregator, + CallbackGenerator callbackGenerator, SignInManager signInManager, UserManager userManager, RoleManager roleManager, diff --git a/BTCPayServer/App/BTCPayAppState.cs b/BTCPayServer/App/BTCPayAppState.cs index 21a0c3af9..736cd8aa1 100644 --- a/BTCPayServer/App/BTCPayAppState.cs +++ b/BTCPayServer/App/BTCPayAppState.cs @@ -147,30 +147,30 @@ public class BTCPayAppState : IHostedService _eventAggregator.SubscribeAsync(UserNotificationsUpdatedEvent)); _compositeDisposable.Add(_eventAggregator.SubscribeAsync(InvoiceChangedEvent)); // User events - _compositeDisposable.Add(_eventAggregator.SubscribeAsync(UserUpdatedEvent)); - _compositeDisposable.Add(_eventAggregator.SubscribeAsync(UserDeletedEvent)); + _compositeDisposable.Add(_eventAggregator.SubscribeAsync(UserUpdatedEvent)); + _compositeDisposable.Add(_eventAggregator.SubscribeAsync(UserDeletedEvent)); // Store events - _compositeDisposable.Add(_eventAggregator.SubscribeAsync(StoreCreatedEvent)); - _compositeDisposable.Add(_eventAggregator.SubscribeAsync(StoreUpdatedEvent)); - _compositeDisposable.Add(_eventAggregator.SubscribeAsync(StoreRemovedEvent)); - _compositeDisposable.Add(_eventAggregator.SubscribeAsync(StoreUserAddedEvent)); - _compositeDisposable.Add(_eventAggregator.SubscribeAsync(StoreUserUpdatedEvent)); - _compositeDisposable.Add(_eventAggregator.SubscribeAsync(StoreUserRemovedEvent)); + _compositeDisposable.Add(_eventAggregator.SubscribeAsync(StoreCreatedEvent)); + _compositeDisposable.Add(_eventAggregator.SubscribeAsync(StoreUpdatedEvent)); + _compositeDisposable.Add(_eventAggregator.SubscribeAsync(StoreRemovedEvent)); + _compositeDisposable.Add(_eventAggregator.SubscribeAsync(StoreUserAddedEvent)); + _compositeDisposable.Add(_eventAggregator.SubscribeAsync(StoreUserUpdatedEvent)); + _compositeDisposable.Add(_eventAggregator.SubscribeAsync(StoreUserRemovedEvent)); // App events - _compositeDisposable.Add(_eventAggregator.SubscribeAsync(AppCreatedEvent)); - _compositeDisposable.Add(_eventAggregator.SubscribeAsync(AppUpdatedEvent)); - _compositeDisposable.Add(_eventAggregator.SubscribeAsync(AppDeletedEvent)); + _compositeDisposable.Add(_eventAggregator.SubscribeAsync(AppCreatedEvent)); + _compositeDisposable.Add(_eventAggregator.SubscribeAsync(AppUpdatedEvent)); + _compositeDisposable.Add(_eventAggregator.SubscribeAsync(AppDeletedEvent)); _ = UpdateNodeInfo(); return Task.CompletedTask; } - private async Task UserUpdatedEvent(UserUpdatedEvent arg) + private async Task UserUpdatedEvent(UserEvent.Updated arg) { - var ev = new ServerEvent { Type = "user-updated", UserId = arg.User.Id, Detail = arg.Detail }; + var ev = new ServerEvent { Type = "user-updated", UserId = arg.User.Id }; await _hubContext.Clients.Group(arg.User.Id).NotifyServerEvent(ev); } - private async Task UserDeletedEvent(UserDeletedEvent arg) + private async Task UserDeletedEvent(UserEvent.Deleted arg) { var ev = new ServerEvent { Type = "user-deleted", UserId = arg.User.Id }; await _hubContext.Clients.Group(arg.User.Id).NotifyServerEvent(ev); @@ -194,7 +194,7 @@ public class BTCPayAppState : IHostedService await _hubContext.Clients.Group(arg.UserId).NotifyServerEvent(ev); } - private async Task StoreCreatedEvent(StoreCreatedEvent arg) + private async Task StoreCreatedEvent(StoreEvent.Created arg) { var ev = new ServerEvent { Type = "store-created", StoreId = arg.StoreId }; @@ -210,33 +210,33 @@ public class BTCPayAppState : IHostedService await _hubContext.Clients.Group(arg.StoreId).NotifyServerEvent(ev); } - private async Task StoreUpdatedEvent(StoreUpdatedEvent arg) + private async Task StoreUpdatedEvent(StoreEvent.Updated arg) { var ev = new ServerEvent { Type ="store-updated", StoreId = arg.StoreId, Detail = arg.Detail }; await _hubContext.Clients.Group(arg.StoreId).NotifyServerEvent(ev); } - private async Task StoreRemovedEvent(StoreRemovedEvent arg) + private async Task StoreRemovedEvent(StoreEvent.Removed arg) { var ev = new ServerEvent { Type = "store-removed", StoreId = arg.StoreId}; await _hubContext.Clients.Group(arg.StoreId).NotifyServerEvent(ev); } - private async Task StoreUserAddedEvent(UserStoreAddedEvent arg) + private async Task StoreUserAddedEvent(UserStoreEvent.Added arg) { var cIds = Connections.Where(pair => pair.Value.UserId == arg.UserId).Select(pair => pair.Key).ToArray(); await AddToGroup(arg.StoreId, cIds); - var ev = new ServerEvent { Type = "user-store-added", StoreId = arg.StoreId, UserId = arg.UserId, Detail = arg.Detail }; + var ev = new ServerEvent { Type = "user-store-added", StoreId = arg.StoreId, UserId = arg.UserId }; await _hubContext.Clients.Groups(arg.StoreId, arg.UserId).NotifyServerEvent(ev); } - private async Task StoreUserUpdatedEvent(UserStoreUpdatedEvent arg) + private async Task StoreUserUpdatedEvent(UserStoreEvent.Updated arg) { - var ev = new ServerEvent { Type = "user-store-updated", StoreId = arg.StoreId, UserId = arg.UserId, Detail = arg.Detail }; + var ev = new ServerEvent { Type = "user-store-updated", StoreId = arg.StoreId, UserId = arg.UserId }; await _hubContext.Clients.Groups(arg.StoreId, arg.UserId).NotifyServerEvent(ev); } - private async Task StoreUserRemovedEvent(UserStoreRemovedEvent arg) + private async Task StoreUserRemovedEvent(UserStoreEvent.Removed arg) { var ev = new ServerEvent { Type = "user-store-removed", StoreId = arg.StoreId, UserId = arg.UserId }; await _hubContext.Clients.Groups(arg.StoreId, arg.UserId).NotifyServerEvent(ev); @@ -245,19 +245,19 @@ public class BTCPayAppState : IHostedService Connections.Where(pair => pair.Value.UserId == arg.UserId).Select(pair => pair.Key).ToArray()); } - private async Task AppCreatedEvent(AppCreatedEvent arg) + private async Task AppCreatedEvent(AppEvent.Created arg) { var ev = new ServerEvent { Type = "app-created", StoreId = arg.StoreId, AppId = arg.AppId, Detail = arg.Detail }; await _hubContext.Clients.Group(arg.StoreId).NotifyServerEvent(ev); } - private async Task AppUpdatedEvent(AppUpdatedEvent arg) + private async Task AppUpdatedEvent(AppEvent.Updated arg) { var ev = new ServerEvent { Type = "app-updated", StoreId = arg.StoreId, AppId = arg.AppId, Detail = arg.Detail }; await _hubContext.Clients.Group(arg.StoreId).NotifyServerEvent(ev); } - private async Task AppDeletedEvent(AppDeletedEvent arg) + private async Task AppDeletedEvent(AppEvent.Deleted arg) { var ev = new ServerEvent { Type = "app-deleted", StoreId = arg.StoreId, AppId = arg.AppId, Detail = arg.Detail }; await _hubContext.Clients.Group(arg.StoreId).NotifyServerEvent(ev); diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldUsersController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldUsersController.cs index 63c1d42aa..94f9ad474 100644 --- a/BTCPayServer/Controllers/GreenField/GreenfieldUsersController.cs +++ b/BTCPayServer/Controllers/GreenField/GreenfieldUsersController.cs @@ -37,6 +37,7 @@ namespace BTCPayServer.Controllers.Greenfield private readonly RoleManager _roleManager; private readonly SettingsRepository _settingsRepository; private readonly EventAggregator _eventAggregator; + private readonly CallbackGenerator _callbackGenerator; private readonly IPasswordValidator _passwordValidator; private readonly IRateLimitService _throttleService; private readonly BTCPayServerOptions _options; @@ -50,6 +51,7 @@ namespace BTCPayServer.Controllers.Greenfield SettingsRepository settingsRepository, PoliciesSettings policiesSettings, EventAggregator eventAggregator, + CallbackGenerator callbackGenerator, IPasswordValidator passwordValidator, IRateLimitService throttleService, BTCPayServerOptions options, @@ -65,6 +67,7 @@ namespace BTCPayServer.Controllers.Greenfield _settingsRepository = settingsRepository; PoliciesSettings = policiesSettings; _eventAggregator = eventAggregator; + _callbackGenerator = callbackGenerator; _passwordValidator = passwordValidator; _throttleService = throttleService; _options = options; @@ -113,7 +116,8 @@ namespace BTCPayServer.Controllers.Greenfield 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() : this.CreateAPIError("invalid-state", $"User is already {(request.Approved ? "approved" : "unapproved")}"); } @@ -221,7 +225,7 @@ namespace BTCPayServer.Controllers.Greenfield } else { - _eventAggregator.Publish(new UserUpdatedEvent(user)); + _eventAggregator.Publish(new UserEvent.Updated(user)); } } @@ -259,7 +263,7 @@ namespace BTCPayServer.Controllers.Greenfield blob.ImageUrl = fileIdUri.ToString(); user.SetBlob(blob); await _userManager.UpdateAsync(user); - _eventAggregator.Publish(new UserUpdatedEvent(user)); + _eventAggregator.Publish(new UserEvent.Updated(user)); var model = await FromModel(user); return Ok(model); } @@ -284,7 +288,7 @@ namespace BTCPayServer.Controllers.Greenfield blob.ImageUrl = null; user.SetBlob(blob); await _userManager.UpdateAsync(user); - _eventAggregator.Publish(new UserUpdatedEvent(user)); + _eventAggregator.Publish(new UserEvent.Updated(user)); } return Ok(); } @@ -404,18 +408,11 @@ namespace BTCPayServer.Controllers.Greenfield await _settingsRepository.FirstAdminRegistered(policies, _options.UpdateUrl != null, _options.DisableRegistration, Logs); } } - var currentUser = await _userManager.GetUserAsync(User); - var userEvent = new UserRegisteredEvent + var userEvent = currentUser switch { - RequestUri = Request.GetAbsoluteRootUri(), - Admin = isNewAdmin, - User = user - }; - if (currentUser is not null) - { - userEvent.Kind = UserRegisteredEventKind.Invite; - userEvent.InvitedByUser = currentUser; + { } invitedBy => await UserEvent.Invited.Create(user, invitedBy, _callbackGenerator, Request, true), + _ => await UserEvent.Registered.Create(user, _callbackGenerator, Request) }; _eventAggregator.Publish(userEvent); @@ -449,7 +446,7 @@ namespace BTCPayServer.Controllers.Greenfield // Ok, this user is an admin but there are other admins as well so safe to delete await _userService.DeleteUserAndAssociatedData(user); - _eventAggregator.Publish(new UserDeletedEvent(user)); + _eventAggregator.Publish(new UserEvent.Deleted(user)); return Ok(); } diff --git a/BTCPayServer/Controllers/UIAccountController.cs b/BTCPayServer/Controllers/UIAccountController.cs index 96aaee309..a5c47c696 100644 --- a/BTCPayServer/Controllers/UIAccountController.cs +++ b/BTCPayServer/Controllers/UIAccountController.cs @@ -41,7 +41,7 @@ namespace BTCPayServer.Controllers readonly SettingsRepository _SettingsRepository; private readonly Fido2Service _fido2Service; private readonly LnurlAuthService _lnurlAuthService; - private readonly LinkGenerator _linkGenerator; + private readonly CallbackGenerator _callbackGenerator; private readonly UserLoginCodeService _userLoginCodeService; private readonly EventAggregator _eventAggregator; readonly ILogger _logger; @@ -64,7 +64,7 @@ namespace BTCPayServer.Controllers UserLoginCodeService userLoginCodeService, LnurlAuthService lnurlAuthService, EmailSenderFactory emailSenderFactory, - LinkGenerator linkGenerator, + CallbackGenerator callbackGenerator, IStringLocalizer stringLocalizer, Logs logs) { @@ -78,8 +78,8 @@ namespace BTCPayServer.Controllers _fido2Service = fido2Service; _lnurlAuthService = lnurlAuthService; EmailSenderFactory = emailSenderFactory; - _linkGenerator = linkGenerator; - _userLoginCodeService = userLoginCodeService; + _callbackGenerator = callbackGenerator; + _userLoginCodeService = userLoginCodeService; _eventAggregator = eventAggregator; _logger = logs.PayServer; Logs = logs; @@ -297,10 +297,7 @@ namespace BTCPayServer.Controllers { RememberMe = rememberMe, UserId = user.Id, - LNURLEndpoint = new Uri(_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) ?? string.Empty) + LNURLEndpoint = new Uri(_callbackGenerator.ForLNUrlAuth(user, r, Request)) }; } return null; @@ -627,12 +624,7 @@ namespace BTCPayServer.Controllers RegisteredAdmin = true; } - _eventAggregator.Publish(new UserRegisteredEvent - { - RequestUri = Request.GetAbsoluteRootUri(), - User = user, - Admin = RegisteredAdmin - }); + _eventAggregator.Publish(await UserEvent.Registered.Create(user, _callbackGenerator, Request)); RegisteredUserId = user.Id; TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Account created."].Value; @@ -699,11 +691,8 @@ namespace BTCPayServer.Controllers var result = await _userManager.ConfirmEmailAsync(user, code); if (result.Succeeded) { - _eventAggregator.Publish(new UserConfirmedEmailEvent - { - User = user, - RequestUri = Request.GetAbsoluteRootUri() - }); + var approvalLink = _callbackGenerator.ForApproval(user, Request); + _eventAggregator.Publish(new UserEvent.ConfirmedEmail(user, approvalLink)); var hasPassword = await _userManager.HasPasswordAsync(user); if (hasPassword) @@ -749,11 +738,8 @@ namespace BTCPayServer.Controllers // Don't reveal that the user does not exist or is not confirmed return RedirectToAction(nameof(ForgotPasswordConfirmation)); } - _eventAggregator.Publish(new UserPasswordResetRequestedEvent - { - User = user, - RequestUri = Request.GetAbsoluteRootUri() - }); + var callbackUri = await _callbackGenerator.ForPasswordReset(user, Request); + _eventAggregator.Publish(new UserEvent.PasswordResetRequested(user, callbackUri)); return RedirectToAction(nameof(ForgotPasswordConfirmation)); } @@ -889,11 +875,10 @@ namespace BTCPayServer.Controllers private async Task FinalizeInvitationIfApplicable(ApplicationUser user) { if (!_userManager.HasInvitationToken(user)) return; - _eventAggregator.Publish(new UserInviteAcceptedEvent - { - User = user, - RequestUri = Request.GetAbsoluteRootUri() - }); + + // This is a placeholder, the real storeIds will be set by the UserEventHostedService + var storeUsersLink = _callbackGenerator.StoreUsersLink("{0}", Request); + _eventAggregator.Publish(new UserEvent.InviteAccepted(user, storeUsersLink)); // unset used token await _userManager.UnsetInvitationTokenAsync(user.Id); } diff --git a/BTCPayServer/Controllers/UIManageController.cs b/BTCPayServer/Controllers/UIManageController.cs index 31f87a97f..01f70ea8e 100644 --- a/BTCPayServer/Controllers/UIManageController.cs +++ b/BTCPayServer/Controllers/UIManageController.cs @@ -36,7 +36,7 @@ namespace BTCPayServer.Controllers private readonly APIKeyRepository _apiKeyRepository; private readonly IAuthorizationService _authorizationService; private readonly Fido2Service _fido2Service; - private readonly LinkGenerator _linkGenerator; + private readonly CallbackGenerator _callbackGenerator; private readonly IHtmlHelper Html; private readonly UserService _userService; private readonly UriResolver _uriResolver; @@ -56,7 +56,7 @@ namespace BTCPayServer.Controllers APIKeyRepository apiKeyRepository, IAuthorizationService authorizationService, Fido2Service fido2Service, - LinkGenerator linkGenerator, + CallbackGenerator callbackGenerator, UserService userService, UriResolver uriResolver, IFileService fileService, @@ -73,7 +73,7 @@ namespace BTCPayServer.Controllers _apiKeyRepository = apiKeyRepository; _authorizationService = authorizationService; _fido2Service = fido2Service; - _linkGenerator = linkGenerator; + _callbackGenerator = callbackGenerator; Html = htmlHelper; _eventAggregator = eventAggregator; _userService = userService; @@ -193,7 +193,7 @@ namespace BTCPayServer.Controllers if (needUpdate && await _userManager.UpdateAsync(user) is { Succeeded: true }) { - _eventAggregator.Publish(new UserUpdatedEvent(user)); + _eventAggregator.Publish(new UserEvent.Updated(user)); TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Your profile has been updated"].Value; } else @@ -219,8 +219,7 @@ namespace BTCPayServer.Controllers throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } - var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); - var callbackUrl = _linkGenerator.EmailConfirmationLink(user.Id, code, Request.Scheme, Request.Host, Request.PathBase); + var callbackUrl = await _callbackGenerator.ForEmailConfirmation(user, Request); (await _EmailSenderFactory.GetEmailSender()).SendEmailConfirmation(user.GetMailboxAddress(), callbackUrl); TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Verification email sent. Please check your email."].Value; return RedirectToAction(nameof(Index)); @@ -332,7 +331,7 @@ namespace BTCPayServer.Controllers } await _userService.DeleteUserAndAssociatedData(user); - _eventAggregator.Publish(new UserDeletedEvent(user)); + _eventAggregator.Publish(new UserEvent.Deleted(user)); TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Account successfully deleted."].Value; await _signInManager.SignOutAsync(); return RedirectToAction(nameof(UIAccountController.Login), "UIAccount"); diff --git a/BTCPayServer/Controllers/UIServerController.Users.cs b/BTCPayServer/Controllers/UIServerController.Users.cs index 678cde1bb..970b02c09 100644 --- a/BTCPayServer/Controllers/UIServerController.Users.cs +++ b/BTCPayServer/Controllers/UIServerController.Users.cs @@ -70,8 +70,7 @@ namespace BTCPayServer.Controllers InvitationUrl = string.IsNullOrEmpty(blob?.InvitationToken) ? null - : _linkGenerator.InvitationLink(u.Id, blob.InvitationToken, Request.Scheme, - Request.Host, Request.PathBase), + : _callbackGenerator.ForInvitation(u, blob.InvitationToken, Request), EmailConfirmed = u.RequiresEmailConfirmation ? u.EmailConfirmed : null, Approved = u.RequiresApproval ? u.Approved : null, Created = u.Created, @@ -98,7 +97,7 @@ namespace BTCPayServer.Controllers Id = user.Id, Email = user.Email, 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)), EmailConfirmed = user.RequiresEmailConfirmation ? user.EmailConfirmed : 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) { - 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) { @@ -260,22 +260,13 @@ namespace BTCPayServer.Controllers if (model.IsAdmin && !(await _UserManager.AddToRoleAsync(user, Roles.ServerAdmin)).Succeeded) model.IsAdmin = false; - var tcs = new TaskCompletionSource(); var currentUser = await _UserManager.GetUserAsync(HttpContext.User); var sendEmail = model.SendInvitationEmail && ViewData["CanSendEmail"] is true; - _eventAggregator.Publish(new UserRegisteredEvent - { - RequestUri = Request.GetAbsoluteRootUri(), - Kind = UserRegisteredEventKind.Invite, - User = user, - InvitedByUser = currentUser, - SendInvitationEmail = sendEmail, - Admin = model.IsAdmin, - CallbackUrlGenerated = tcs - }); + var evt = await UserEvent.Invited.Create(user, currentUser, _callbackGenerator, Request, sendEmail); + _eventAggregator.Publish(evt); + - var callbackUrl = await tcs.Task; var info = sendEmail ? "An invitation email has been sent. You may alternatively" : "An invitation email has not been sent. You need to"; @@ -284,7 +275,7 @@ namespace BTCPayServer.Controllers { Severity = StatusMessageModel.StatusSeverity.Success, AllowDismiss = false, - Html = $"Account successfully created. {info} share this link with them:
{callbackUrl}" + Html = $"Account successfully created. {info} share this link with them:
{evt.InvitationLink}" }); return RedirectToAction(nameof(User), new { userId = user.Id }); } @@ -387,7 +378,8 @@ namespace BTCPayServer.Controllers if (user == null) 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 ? StringLocalizer["User approved"].Value @@ -414,8 +406,7 @@ namespace BTCPayServer.Controllers throw new ApplicationException($"Unable to load user with ID '{userId}'."); } - var code = await _UserManager.GenerateEmailConfirmationTokenAsync(user); - var callbackUrl = _linkGenerator.EmailConfirmationLink(user.Id, code, Request.Scheme, Request.Host, Request.PathBase); + var callbackUrl = await _callbackGenerator.ForEmailConfirmation(user, Request); (await _emailSenderFactory.GetEmailSender()).SendEmailConfirmation(user.GetMailboxAddress(), callbackUrl); diff --git a/BTCPayServer/Controllers/UIServerController.cs b/BTCPayServer/Controllers/UIServerController.cs index f97cfcef0..0a2071857 100644 --- a/BTCPayServer/Controllers/UIServerController.cs +++ b/BTCPayServer/Controllers/UIServerController.cs @@ -64,7 +64,7 @@ namespace BTCPayServer.Controllers private readonly StoredFileRepository _StoredFileRepository; private readonly IFileService _fileService; private readonly IEnumerable _StorageProviderServices; - private readonly LinkGenerator _linkGenerator; + private readonly CallbackGenerator _callbackGenerator; private readonly UriResolver _uriResolver; private readonly EmailSenderFactory _emailSenderFactory; private readonly TransactionLinkProviders _transactionLinkProviders; @@ -90,7 +90,7 @@ namespace BTCPayServer.Controllers EventAggregator eventAggregator, IOptions externalServiceOptions, Logs logs, - LinkGenerator linkGenerator, + CallbackGenerator callbackGenerator, UriResolver uriResolver, EmailSenderFactory emailSenderFactory, IHostApplicationLifetime applicationLifetime, @@ -119,7 +119,7 @@ namespace BTCPayServer.Controllers _eventAggregator = eventAggregator; _externalServiceOptions = externalServiceOptions; Logs = logs; - _linkGenerator = linkGenerator; + _callbackGenerator = callbackGenerator; _uriResolver = uriResolver; _emailSenderFactory = emailSenderFactory; ApplicationLifetime = applicationLifetime; diff --git a/BTCPayServer/Controllers/UIStoresController.Users.cs b/BTCPayServer/Controllers/UIStoresController.Users.cs index 03fa7048c..c222baa6f 100644 --- a/BTCPayServer/Controllers/UIStoresController.Users.cs +++ b/BTCPayServer/Controllers/UIStoresController.Users.cs @@ -9,6 +9,7 @@ using BTCPayServer.Client; using BTCPayServer.Data; using BTCPayServer.Events; using BTCPayServer.Models.StoreViewModels; +using BTCPayServer.Services; using BTCPayServer.Services.Mails; using BTCPayServer.Services.Stores; using Microsoft.AspNetCore.Authorization; @@ -58,28 +59,18 @@ public partial class UIStoresController Created = DateTimeOffset.UtcNow }; - var result = await _userManager.CreateAsync(user); - if (result.Succeeded) + var currentUser = await _userManager.GetUserAsync(HttpContext.User); + if (currentUser is not null && + (await _userManager.CreateAsync(user)) is { Succeeded: true } result) { var invitationEmail = await _emailSenderFactory.IsComplete(); - var tcs = new TaskCompletionSource(); - var currentUser = await _userManager.GetUserAsync(HttpContext.User); + var evt = await UserEvent.Invited.Create(user, currentUser, _callbackGenerator, Request, invitationEmail); + _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 ? "An invitation email has been sent.
You may alternatively" : "An invitation email has not been sent, because the server does not have an email server configured.
You need to"; - successInfo = $"{info} share this link with them: {callbackUrl}"; + successInfo = $"{info} share this link with them: {evt.InvitationLink}"; } else { diff --git a/BTCPayServer/Controllers/UIStoresController.cs b/BTCPayServer/Controllers/UIStoresController.cs index 228f0f48d..e8727270e 100644 --- a/BTCPayServer/Controllers/UIStoresController.cs +++ b/BTCPayServer/Controllers/UIStoresController.cs @@ -59,6 +59,7 @@ public partial class UIStoresController : Controller EmailSenderFactory emailSenderFactory, WalletFileParsers onChainWalletParsers, UIUserStoresController userStoresController, + CallbackGenerator callbackGenerator, UriResolver uriResolver, CurrencyNameTable currencyNameTable, IStringLocalizer stringLocalizer, @@ -86,6 +87,7 @@ public partial class UIStoresController : Controller _emailSenderFactory = emailSenderFactory; _onChainWalletParsers = onChainWalletParsers; _userStoresController = userStoresController; + _callbackGenerator = callbackGenerator; _uriResolver = uriResolver; _currencyNameTable = currencyNameTable; _eventAggregator = eventAggregator; @@ -121,6 +123,7 @@ public partial class UIStoresController : Controller private readonly EmailSenderFactory _emailSenderFactory; private readonly WalletFileParsers _onChainWalletParsers; private readonly UIUserStoresController _userStoresController; + private readonly CallbackGenerator _callbackGenerator; private readonly UriResolver _uriResolver; private readonly EventAggregator _eventAggregator; private readonly IHtmlHelper _html; diff --git a/BTCPayServer/Events/AppCreatedEvent.cs b/BTCPayServer/Events/AppCreatedEvent.cs deleted file mode 100644 index 18b499b35..000000000 --- a/BTCPayServer/Events/AppCreatedEvent.cs +++ /dev/null @@ -1,12 +0,0 @@ -#nullable enable -using BTCPayServer.Data; - -namespace BTCPayServer.Events; - -public class AppCreatedEvent(AppData app, string? detail = null) : AppEvent(app, detail ?? app.AppType) -{ - protected override string ToString() - { - return $"{base.ToString()} has been created"; - } -} diff --git a/BTCPayServer/Events/AppDeletedEvent.cs b/BTCPayServer/Events/AppDeletedEvent.cs deleted file mode 100644 index bef6b6fb9..000000000 --- a/BTCPayServer/Events/AppDeletedEvent.cs +++ /dev/null @@ -1,12 +0,0 @@ -#nullable enable -using BTCPayServer.Data; - -namespace BTCPayServer.Events; - -public class AppDeletedEvent(AppData app, string? detail = null) : AppEvent(app, detail ?? app.AppType) -{ - protected override string ToString() - { - return $"{base.ToString()} has been deleted"; - } -} diff --git a/BTCPayServer/Events/AppEvent.cs b/BTCPayServer/Events/AppEvent.cs index 90948418b..898b22a21 100644 --- a/BTCPayServer/Events/AppEvent.cs +++ b/BTCPayServer/Events/AppEvent.cs @@ -5,6 +5,27 @@ 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; diff --git a/BTCPayServer/Events/AppUpdatedEvent.cs b/BTCPayServer/Events/AppUpdatedEvent.cs deleted file mode 100644 index b7a8402d6..000000000 --- a/BTCPayServer/Events/AppUpdatedEvent.cs +++ /dev/null @@ -1,12 +0,0 @@ -#nullable enable -using BTCPayServer.Data; - -namespace BTCPayServer.Events; - -public class AppUpdatedEvent(AppData app, string? detail = null) : AppEvent(app, detail ?? app.AppType) -{ - protected override string ToString() - { - return $"{base.ToString()} has been updated"; - } -} diff --git a/BTCPayServer/Events/NewBlockEvent.cs b/BTCPayServer/Events/NewBlockEvent.cs index 4ec5a62e2..3fa5c14a1 100644 --- a/BTCPayServer/Events/NewBlockEvent.cs +++ b/BTCPayServer/Events/NewBlockEvent.cs @@ -5,6 +5,7 @@ namespace BTCPayServer.Events public class NewBlockEvent : NBXplorer.Models.NewBlockEvent { public PaymentMethodId PaymentMethodId { get; set; } + public object AdditionalInfo { get; set; } public override string ToString() { return $"{PaymentMethodId}: New block"; diff --git a/BTCPayServer/Events/StoreCreatedEvent.cs b/BTCPayServer/Events/StoreCreatedEvent.cs deleted file mode 100644 index 15377d0fd..000000000 --- a/BTCPayServer/Events/StoreCreatedEvent.cs +++ /dev/null @@ -1,12 +0,0 @@ -#nullable enable -using BTCPayServer.Data; - -namespace BTCPayServer.Events; - -public class StoreCreatedEvent(StoreData store, string? detail = null) : StoreEvent(store, detail) -{ - protected override string ToString() - { - return $"{base.ToString()} has been created"; - } -} diff --git a/BTCPayServer/Events/StoreEvent.cs b/BTCPayServer/Events/StoreEvent.cs index 6a10a6dbe..b68d6f6fd 100644 --- a/BTCPayServer/Events/StoreEvent.cs +++ b/BTCPayServer/Events/StoreEvent.cs @@ -7,6 +7,27 @@ 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; diff --git a/BTCPayServer/Events/StoreRemovedEvent.cs b/BTCPayServer/Events/StoreRemovedEvent.cs deleted file mode 100644 index 90483f4af..000000000 --- a/BTCPayServer/Events/StoreRemovedEvent.cs +++ /dev/null @@ -1,12 +0,0 @@ -#nullable enable -using BTCPayServer.Data; - -namespace BTCPayServer.Events; - -public class StoreRemovedEvent(StoreData store, string? detail = null) : StoreEvent(store, detail) -{ - protected override string ToString() - { - return $"{base.ToString()} has been removed"; - } -} diff --git a/BTCPayServer/Events/StoreUpdatedEvent.cs b/BTCPayServer/Events/StoreUpdatedEvent.cs deleted file mode 100644 index 3a876f69f..000000000 --- a/BTCPayServer/Events/StoreUpdatedEvent.cs +++ /dev/null @@ -1,12 +0,0 @@ -#nullable enable -using BTCPayServer.Data; - -namespace BTCPayServer.Events; - -public class StoreUpdatedEvent(StoreData store, string? detail = null) : StoreEvent(store, detail) -{ - protected override string ToString() - { - return $"{base.ToString()} has been updated"; - } -} diff --git a/BTCPayServer/Events/UserApprovedEvent.cs b/BTCPayServer/Events/UserApprovedEvent.cs deleted file mode 100644 index 87264167b..000000000 --- a/BTCPayServer/Events/UserApprovedEvent.cs +++ /dev/null @@ -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; } -} diff --git a/BTCPayServer/Events/UserConfirmedEmailEvent.cs b/BTCPayServer/Events/UserConfirmedEmailEvent.cs deleted file mode 100644 index dc9c69055..000000000 --- a/BTCPayServer/Events/UserConfirmedEmailEvent.cs +++ /dev/null @@ -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; } -} diff --git a/BTCPayServer/Events/UserDeletedEvent.cs b/BTCPayServer/Events/UserDeletedEvent.cs deleted file mode 100644 index 527b593be..000000000 --- a/BTCPayServer/Events/UserDeletedEvent.cs +++ /dev/null @@ -1,12 +0,0 @@ -using BTCPayServer.Data; - -namespace BTCPayServer.Events; - -public class UserDeletedEvent(ApplicationUser user) : UserEvent(user) -{ - protected override string ToString() - { - return $"{base.ToString()} has been deleted"; - } -} - diff --git a/BTCPayServer/Events/UserEvent.cs b/BTCPayServer/Events/UserEvent.cs index 71396ab49..6e5554663 100644 --- a/BTCPayServer/Events/UserEvent.cs +++ b/BTCPayServer/Events/UserEvent.cs @@ -1,12 +1,84 @@ #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, string? detail = null) +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 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 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; - public string? Detail { get; } = detail; protected new virtual string ToString() { diff --git a/BTCPayServer/Events/UserInviteAcceptedEvent.cs b/BTCPayServer/Events/UserInviteAcceptedEvent.cs deleted file mode 100644 index 9af8174ca..000000000 --- a/BTCPayServer/Events/UserInviteAcceptedEvent.cs +++ /dev/null @@ -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; } -} diff --git a/BTCPayServer/Events/UserPasswordResetRequestedEvent.cs b/BTCPayServer/Events/UserPasswordResetRequestedEvent.cs deleted file mode 100644 index 9758315b1..000000000 --- a/BTCPayServer/Events/UserPasswordResetRequestedEvent.cs +++ /dev/null @@ -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 CallbackUrlGenerated; - } -} diff --git a/BTCPayServer/Events/UserRegisteredEvent.cs b/BTCPayServer/Events/UserRegisteredEvent.cs deleted file mode 100644 index 16d136e8f..000000000 --- a/BTCPayServer/Events/UserRegisteredEvent.cs +++ /dev/null @@ -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 CallbackUrlGenerated; -} - -public enum UserRegisteredEventKind -{ - Registration, - Invite -} diff --git a/BTCPayServer/Events/UserStoreAddedEvent.cs b/BTCPayServer/Events/UserStoreAddedEvent.cs deleted file mode 100644 index 2db924e25..000000000 --- a/BTCPayServer/Events/UserStoreAddedEvent.cs +++ /dev/null @@ -1,10 +0,0 @@ -#nullable enable -namespace BTCPayServer.Events; - -public class UserStoreAddedEvent(string storeId, string userId, string? detail = null) : UserStoreEvent(storeId, userId, detail) -{ - protected override string ToString() - { - return $"{base.ToString()} has been added"; - } -} diff --git a/BTCPayServer/Events/UserStoreEvent.cs b/BTCPayServer/Events/UserStoreEvent.cs index f3cf8764c..11ea084f6 100644 --- a/BTCPayServer/Events/UserStoreEvent.cs +++ b/BTCPayServer/Events/UserStoreEvent.cs @@ -1,11 +1,36 @@ #nullable enable +using BTCPayServer.Migrations; + namespace BTCPayServer.Events; -public abstract class UserStoreEvent(string storeId, string userId, string? detail = null) +public abstract class UserStoreEvent(string storeId, string userId) { + public class Added(string storeId, string userId, string roleId) : UserStoreEvent(storeId, userId) + { + public string RoleId { get; } = roleId; + protected override string ToString() + { + return $"{base.ToString()} has been added"; + } + } + public class Removed(string storeId, string userId) : UserStoreEvent(storeId, userId) + { + protected override string ToString() + { + return $"{base.ToString()} has been removed"; + } + } + public class Updated(string storeId, string userId, string roleId) : UserStoreEvent(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; - public string? Detail { get; } = detail; protected new virtual string ToString() { diff --git a/BTCPayServer/Events/UserStoreRemovedEvent.cs b/BTCPayServer/Events/UserStoreRemovedEvent.cs deleted file mode 100644 index 239741de3..000000000 --- a/BTCPayServer/Events/UserStoreRemovedEvent.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace BTCPayServer.Events; - -public class UserStoreRemovedEvent(string storeId, string userId) : UserStoreEvent(storeId, userId) -{ - protected override string ToString() - { - return $"{base.ToString()} has been removed"; - } -} diff --git a/BTCPayServer/Events/UserStoreUpdatedEvent.cs b/BTCPayServer/Events/UserStoreUpdatedEvent.cs deleted file mode 100644 index c94a4da97..000000000 --- a/BTCPayServer/Events/UserStoreUpdatedEvent.cs +++ /dev/null @@ -1,10 +0,0 @@ -#nullable enable -namespace BTCPayServer.Events; - -public class UserStoreUpdatedEvent(string storeId, string userId, string? detail = null) : UserStoreEvent(storeId, userId, detail) -{ - protected override string ToString() - { - return $"{base.ToString()} has been updated"; - } -} diff --git a/BTCPayServer/Events/UserUpdatedEvent.cs b/BTCPayServer/Events/UserUpdatedEvent.cs deleted file mode 100644 index be07280ee..000000000 --- a/BTCPayServer/Events/UserUpdatedEvent.cs +++ /dev/null @@ -1,12 +0,0 @@ -#nullable enable -using BTCPayServer.Data; - -namespace BTCPayServer.Events; - -public class UserUpdatedEvent(ApplicationUser user, string? detail = null) : UserEvent(user, detail) -{ - protected override string ToString() - { - return $"{base.ToString()} has been updated"; - } -} diff --git a/BTCPayServer/Extensions/UrlHelperExtensions.cs b/BTCPayServer/Extensions/UrlHelperExtensions.cs index 16e2a0aa5..8f22a50b1 100644 --- a/BTCPayServer/Extensions/UrlHelperExtensions.cs +++ b/BTCPayServer/Extensions/UrlHelperExtensions.cs @@ -23,52 +23,11 @@ namespace Microsoft.AspNetCore.Mvc } #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) { 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) { return urlHelper.GetUriByAction( diff --git a/BTCPayServer/HostedServices/UserEventHostedService.cs b/BTCPayServer/HostedServices/UserEventHostedService.cs index 041eb873b..4052f3598 100644 --- a/BTCPayServer/HostedServices/UserEventHostedService.cs +++ b/BTCPayServer/HostedServices/UserEventHostedService.cs @@ -20,41 +20,40 @@ namespace BTCPayServer.HostedServices; public class UserEventHostedService( EventAggregator eventAggregator, UserManager userManager, + CallbackGenerator callbackGenerator, EmailSenderFactory emailSenderFactory, NotificationSender notificationSender, StoreRepository storeRepository, - LinkGenerator generator, Logs logs) : EventHostedServiceBase(eventAggregator, logs) { + public UserManager UserManager { get; } = userManager; + public CallbackGenerator CallbackGenerator { get; } = callbackGenerator; + protected override void SubscribeToEvents() { - Subscribe(); - Subscribe(); - Subscribe(); - Subscribe(); - Subscribe(); + Subscribe(); + Subscribe(); + Subscribe(); + Subscribe(); + Subscribe(); } protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken) { - string code; - string callbackUrl; - Uri uri; - HostString host; - ApplicationUser user; + ApplicationUser user = (evt as UserEvent).User; IEmailSender emailSender; switch (evt) { - case UserRegisteredEvent ev: - user = ev.User; - uri = ev.RequestUri; - host = new HostString(uri.Host, uri.Port); - + case UserEvent.Registered ev: // can be either a self-registration or by invite from another user - var isInvite = ev.Kind == UserRegisteredEventKind.Invite; - var type = ev.Admin ? "admin" : "user"; - var info = isInvite ? ev.InvitedByUser != null ? $"invited by {ev.InvitedByUser.Email}" : "invited" : "registered"; + var type = await UserManager.IsInRoleAsync(user, Roles.ServerAdmin) ? "admin" : "user"; + var info = ev switch + { + UserEvent.Invited { InvitedByUser: { } invitedBy } => $"invited by {invitedBy.Email}", + UserEvent.Invited => "invited", + _ => "registered" + }; var requiresApproval = user.RequiresApproval && !user.Approved; var requiresEmailConfirmation = user.RequiresEmailConfirmation && !user.EmailConfirmed; @@ -66,84 +65,55 @@ public class UserEventHostedService( // inform admins only about qualified users and not annoy them with bot registrations. if (requiresApproval && !requiresEmailConfirmation) { - await NotifyAdminsAboutUserRequiringApproval(user, uri, newUserInfo); + await NotifyAdminsAboutUserRequiringApproval(user, ev.ApprovalLink, newUserInfo); } // set callback result and send email to user emailSender = await emailSenderFactory.GetEmailSender(); - if (isInvite) + if (ev is UserEvent.Invited invited) { - code = await userManager.GenerateInvitationTokenAsync(user.Id); - callbackUrl = generator.InvitationLink(user.Id, code, uri.Scheme, host, uri.PathAndQuery); - ev.CallbackUrlGenerated?.SetResult(new Uri(callbackUrl)); - - if (ev.SendInvitationEmail) - emailSender.SendInvitation(user.GetMailboxAddress(), callbackUrl); + if (invited.SendInvitationEmail) + emailSender.SendInvitation(user.GetMailboxAddress(), invited.InvitationLink); } else if (requiresEmailConfirmation) { - code = await userManager.GenerateEmailConfirmationTokenAsync(user); - 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); + emailSender.SendEmailConfirmation(user.GetMailboxAddress(), ev.ConfirmationEmailLink); } break; - case UserPasswordResetRequestedEvent 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)); + case UserEvent.PasswordResetRequested pwResetEvent: Logs.PayServer.LogInformation("User {Email} requested a password reset", user.Email); emailSender = await emailSenderFactory.GetEmailSender(); - emailSender.SendResetPassword(user.GetMailboxAddress(), callbackUrl); + emailSender.SendResetPassword(user.GetMailboxAddress(), pwResetEvent.ResetLink); break; - case UserApprovedEvent approvedEvent: - user = approvedEvent.User; + case UserEvent.Approved approvedEvent: 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.SendApprovalConfirmation(user.GetMailboxAddress(), callbackUrl); + emailSender.SendApprovalConfirmation(user.GetMailboxAddress(), approvedEvent.LoginLink); break; - case UserConfirmedEmailEvent confirmedEvent: - user = confirmedEvent.User; + case UserEvent.ConfirmedEmail confirmedEvent: if (!user.EmailConfirmed) break; - uri = confirmedEvent.RequestUri; var confirmedUserInfo = $"User {user.Email} confirmed their email address"; Logs.PayServer.LogInformation(confirmedUserInfo); - if (!user.RequiresApproval || user.Approved) return; - await NotifyAdminsAboutUserRequiringApproval(user, uri, confirmedUserInfo); + await NotifyAdminsAboutUserRequiringApproval(user, confirmedEvent.ApprovalLink, confirmedUserInfo); break; - case UserInviteAcceptedEvent inviteAcceptedEvent: - user = inviteAcceptedEvent.User; - uri = inviteAcceptedEvent.RequestUri; + case UserEvent.InviteAccepted inviteAcceptedEvent: Logs.PayServer.LogInformation("User {Email} accepted the invite", user.Email); - await NotifyAboutUserAcceptingInvite(user, uri); + await NotifyAboutUserAcceptingInvite(user, inviteAcceptedEvent.StoreUsersLink); 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; // notification await notificationSender.SendNotification(new AdminScope(), new NewUserRequiresApprovalNotification(user)); // email - 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 admins = await UserManager.GetUsersInRoleAsync(Roles.ServerAdmin); var emailSender = await emailSenderFactory.GetEmailSender(); foreach (var admin in admins) { @@ -151,7 +121,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 notifyRoles = new[] { StoreRoleId.Owner, StoreRoleId.Manager }; @@ -161,15 +131,14 @@ public class UserEventHostedService( await notificationSender.SendNotification(new StoreScope(store.Id, notifyRoles), new InviteAcceptedNotification(user, store)); // email var notifyUsers = await storeRepository.GetStoreUsers(store.Id, notifyRoles); - var host = new HostString(uri.Host, uri.Port); - var storeUsersLink = generator.StoreUsersLink(store.Id, uri.Scheme, host, uri.PathAndQuery); + var link = string.Format(storeUsersLink, store.Id); var emailSender = await emailSenderFactory.GetEmailSender(store.Id); 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) - 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}"; - emailSender.SendUserInviteAcceptedInfo(notifyUser.GetMailboxAddress(), info, storeUsersLink); + emailSender.SendUserInviteAcceptedInfo(notifyUser.GetMailboxAddress(), info, link); } } } diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index 0eaf1cc4c..e71c468fa 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -86,6 +86,7 @@ namespace BTCPayServer.Hosting } public static IServiceCollection AddBTCPayServer(this IServiceCollection services, IConfiguration configuration, Logs logs) { + services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); diff --git a/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs b/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs index 9e5d3420b..a0c75b79e 100644 --- a/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs +++ b/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs @@ -154,16 +154,7 @@ namespace BTCPayServer.Payments.Bitcoin { case NBXplorer.Models.NewBlockEvent evt: await UpdatePaymentStates(wallet); - _Aggregator.Publish(new Events.NewBlockEvent - { - PaymentMethodId = pmi, - Confirmations = evt.Confirmations, - Hash = evt.Hash, - Height = evt.Height, - EventId = evt.EventId, - PreviousBlockHash = evt.PreviousBlockHash - } - ); + _Aggregator.Publish(new Events.NewBlockEvent() { PaymentMethodId = pmi, AdditionalInfo = evt }); break; case NBXplorer.Models.NewTransactionEvent evt: if (evt.DerivationStrategy != null) diff --git a/BTCPayServer/Services/Apps/AppService.cs b/BTCPayServer/Services/Apps/AppService.cs index bf5b503b1..9a5e65d26 100644 --- a/BTCPayServer/Services/Apps/AppService.cs +++ b/BTCPayServer/Services/Apps/AppService.cs @@ -233,7 +233,7 @@ namespace BTCPayServer.Services.Apps await using var ctx = _ContextFactory.CreateContext(); ctx.Apps.Add(appData); ctx.Entry(appData).State = EntityState.Deleted; - _eventAggregator.Publish(new AppDeletedEvent(appData)); + _eventAggregator.Publish(new AppEvent.Deleted(appData)); return await ctx.SaveChangesAsync() == 1; } @@ -242,7 +242,7 @@ namespace BTCPayServer.Services.Apps await using var ctx = _ContextFactory.CreateContext(); appData.Archived = archived; ctx.Entry(appData).State = EntityState.Modified; - _eventAggregator.Publish(new AppUpdatedEvent(appData)); + _eventAggregator.Publish(new AppEvent.Updated(appData)); return await ctx.SaveChangesAsync() == 1; } @@ -466,9 +466,9 @@ retry: } await ctx.SaveChangesAsync(); if (newApp) - _eventAggregator.Publish(new AppCreatedEvent(app)); + _eventAggregator.Publish(new AppEvent.Created(app)); else - _eventAggregator.Publish(new AppUpdatedEvent(app)); + _eventAggregator.Publish(new AppEvent.Updated(app)); } private static bool TryParseJson(string json, [MaybeNullWhen(false)] out JObject result) diff --git a/BTCPayServer/Services/CallbackGenerator.cs b/BTCPayServer/Services/CallbackGenerator.cs new file mode 100644 index 000000000..f8e3924fb --- /dev/null +++ b/BTCPayServer/Services/CallbackGenerator.cs @@ -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 userManager) + { + public LinkGenerator LinkGenerator { get; } = linkGenerator; + public UserManager 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 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 ForInvitation(ApplicationUser user, HttpRequest request) + { + var code = await UserManager.GenerateInvitationTokenAsync(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 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)"); + } +} diff --git a/BTCPayServer/Services/Stores/StoreRepository.cs b/BTCPayServer/Services/Stores/StoreRepository.cs index 03e8aadcc..db9a54ec1 100644 --- a/BTCPayServer/Services/Stores/StoreRepository.cs +++ b/BTCPayServer/Services/Stores/StoreRepository.cs @@ -298,7 +298,7 @@ namespace BTCPayServer.Services.Stores try { await ctx.SaveChangesAsync(); - _eventAggregator.Publish(new UserStoreAddedEvent(storeId, userId)); + _eventAggregator.Publish(new UserStoreEvent.Added(storeId, userId, roleId.Id)); return true; } catch (DbUpdateException) @@ -330,8 +330,8 @@ namespace BTCPayServer.Services.Stores { await ctx.SaveChangesAsync(); UserStoreEvent evt = added - ? new UserStoreAddedEvent(storeId, userId, userStore.StoreRoleId) - : new UserStoreUpdatedEvent(storeId, userId, userStore.StoreRoleId); + ? new UserStoreEvent.Added(storeId, userId, userStore.StoreRoleId) + : new UserStoreEvent.Updated(storeId, userId, userStore.StoreRoleId); _eventAggregator.Publish(evt); return true; } @@ -347,22 +347,6 @@ namespace BTCPayServer.Services.Stores 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(); - 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 StoreRemovedEvent(store)); - } - await ctx.SaveChangesAsync(); - events.ForEach(e => _eventAggregator.Publish(e)); - } - public async Task RemoveStoreUser(string storeId, string userId) { await using var ctx = _ContextFactory.CreateContext(); @@ -374,7 +358,7 @@ namespace BTCPayServer.Services.Stores ctx.UserStore.Add(userStore); ctx.Entry(userStore).State = EntityState.Deleted; await ctx.SaveChangesAsync(); - _eventAggregator.Publish(new UserStoreRemovedEvent(storeId, userId)); + _eventAggregator.Publish(new UserStoreEvent.Removed(storeId, userId)); return true; } @@ -388,7 +372,7 @@ namespace BTCPayServer.Services.Stores { ctx.Stores.Remove(store); await ctx.SaveChangesAsync(); - _eventAggregator.Publish(new StoreRemovedEvent(store)); + _eventAggregator.Publish(new StoreEvent.Removed(store)); } } } @@ -414,8 +398,8 @@ namespace BTCPayServer.Services.Stores ctx.Add(storeData); ctx.Add(userStore); await ctx.SaveChangesAsync(); - _eventAggregator.Publish(new UserStoreAddedEvent(storeData.Id, userStore.ApplicationUserId)); - _eventAggregator.Publish(new StoreCreatedEvent(storeData)); + _eventAggregator.Publish(new UserStoreEvent.Added(storeData.Id, userStore.ApplicationUserId, roleId.Id)); + _eventAggregator.Publish(new StoreEvent.Created(storeData)); } public async Task GetWebhooks(string storeId) @@ -564,7 +548,7 @@ namespace BTCPayServer.Services.Stores { ctx.Entry(existing).CurrentValues.SetValues(store); await ctx.SaveChangesAsync().ConfigureAwait(false); - _eventAggregator.Publish(new StoreUpdatedEvent(store)); + _eventAggregator.Publish(new StoreEvent.Updated(store)); } } @@ -587,7 +571,7 @@ retry: { await ctx.SaveChangesAsync(); if (store != null) - _eventAggregator.Publish(new StoreRemovedEvent(store)); + _eventAggregator.Publish(new StoreEvent.Removed(store)); } catch (DbUpdateException ex) when (IsDeadlock(ex) && retry < 5) { diff --git a/BTCPayServer/Services/UserService.cs b/BTCPayServer/Services/UserService.cs index 1496c3fb5..7de613c1e 100644 --- a/BTCPayServer/Services/UserService.cs +++ b/BTCPayServer/Services/UserService.cs @@ -108,7 +108,7 @@ namespace BTCPayServer.Services return true; } - public async Task SetUserApproval(string userId, bool approved, Uri requestUri) + public async Task SetUserApproval(string userId, bool approved, string loginLink) { using var scope = _serviceProvider.CreateScope(); var userManager = scope.ServiceProvider.GetRequiredService>(); @@ -123,7 +123,7 @@ namespace BTCPayServer.Services if (succeeded) { _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 {