Merge Nicolas' event refactoring

This commit is contained in:
Dennis Reimann 2024-12-09 12:29:17 +01:00
parent 0876723e1b
commit b3a7739eaa
No known key found for this signature in database
GPG key ID: 5009E1797F03F8D0
40 changed files with 373 additions and 482 deletions

View file

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

View file

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

View file

@ -30,6 +30,7 @@ public partial class AppApiController(
StoreRepository storeRepository,
AppService appService,
EventAggregator eventAggregator,
CallbackGenerator callbackGenerator,
SignInManager<ApplicationUser> signInManager,
UserManager<ApplicationUser> userManager,
RoleManager<IdentityRole> roleManager,

View file

@ -147,30 +147,30 @@ public class BTCPayAppState : IHostedService
_eventAggregator.SubscribeAsync<UserNotificationsUpdatedEvent>(UserNotificationsUpdatedEvent));
_compositeDisposable.Add(_eventAggregator.SubscribeAsync<InvoiceEvent>(InvoiceChangedEvent));
// User events
_compositeDisposable.Add(_eventAggregator.SubscribeAsync<UserUpdatedEvent>(UserUpdatedEvent));
_compositeDisposable.Add(_eventAggregator.SubscribeAsync<UserDeletedEvent>(UserDeletedEvent));
_compositeDisposable.Add(_eventAggregator.SubscribeAsync<UserEvent.Updated>(UserUpdatedEvent));
_compositeDisposable.Add(_eventAggregator.SubscribeAsync<UserEvent.Deleted>(UserDeletedEvent));
// Store events
_compositeDisposable.Add(_eventAggregator.SubscribeAsync<StoreCreatedEvent>(StoreCreatedEvent));
_compositeDisposable.Add(_eventAggregator.SubscribeAsync<StoreUpdatedEvent>(StoreUpdatedEvent));
_compositeDisposable.Add(_eventAggregator.SubscribeAsync<StoreRemovedEvent>(StoreRemovedEvent));
_compositeDisposable.Add(_eventAggregator.SubscribeAsync<UserStoreAddedEvent>(StoreUserAddedEvent));
_compositeDisposable.Add(_eventAggregator.SubscribeAsync<UserStoreUpdatedEvent>(StoreUserUpdatedEvent));
_compositeDisposable.Add(_eventAggregator.SubscribeAsync<UserStoreRemovedEvent>(StoreUserRemovedEvent));
_compositeDisposable.Add(_eventAggregator.SubscribeAsync<StoreEvent.Created>(StoreCreatedEvent));
_compositeDisposable.Add(_eventAggregator.SubscribeAsync<StoreEvent.Updated>(StoreUpdatedEvent));
_compositeDisposable.Add(_eventAggregator.SubscribeAsync<StoreEvent.Removed>(StoreRemovedEvent));
_compositeDisposable.Add(_eventAggregator.SubscribeAsync<UserStoreEvent.Added>(StoreUserAddedEvent));
_compositeDisposable.Add(_eventAggregator.SubscribeAsync<UserStoreEvent.Updated>(StoreUserUpdatedEvent));
_compositeDisposable.Add(_eventAggregator.SubscribeAsync<UserStoreEvent.Removed>(StoreUserRemovedEvent));
// App events
_compositeDisposable.Add(_eventAggregator.SubscribeAsync<AppCreatedEvent>(AppCreatedEvent));
_compositeDisposable.Add(_eventAggregator.SubscribeAsync<AppUpdatedEvent>(AppUpdatedEvent));
_compositeDisposable.Add(_eventAggregator.SubscribeAsync<AppDeletedEvent>(AppDeletedEvent));
_compositeDisposable.Add(_eventAggregator.SubscribeAsync<AppEvent.Created>(AppCreatedEvent));
_compositeDisposable.Add(_eventAggregator.SubscribeAsync<AppEvent.Updated>(AppUpdatedEvent));
_compositeDisposable.Add(_eventAggregator.SubscribeAsync<AppEvent.Deleted>(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);

View file

@ -37,6 +37,7 @@ namespace BTCPayServer.Controllers.Greenfield
private readonly RoleManager<IdentityRole> _roleManager;
private readonly SettingsRepository _settingsRepository;
private readonly EventAggregator _eventAggregator;
private readonly CallbackGenerator _callbackGenerator;
private readonly IPasswordValidator<ApplicationUser> _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<ApplicationUser> 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();
}

View file

@ -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<ApplicationUser>(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<ApplicationUser>(user.Id);
}

View file

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

View file

@ -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<Uri>();
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:<br/>{callbackUrl}"
Html = $"Account successfully created. {info} share this link with them:<br/>{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);

View file

@ -64,7 +64,7 @@ namespace BTCPayServer.Controllers
private readonly StoredFileRepository _StoredFileRepository;
private readonly IFileService _fileService;
private readonly IEnumerable<IStorageProviderService> _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<ExternalServicesOptions> 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;

View file

@ -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<Uri>();
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.<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";
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
{

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

View file

@ -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<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;
public string? Detail { get; } = detail;
protected new virtual string ToString()
{

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

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

View file

@ -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()
{

View file

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

View file

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

View file

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

View file

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

View file

@ -20,41 +20,40 @@ namespace BTCPayServer.HostedServices;
public class UserEventHostedService(
EventAggregator eventAggregator,
UserManager<ApplicationUser> userManager,
CallbackGenerator callbackGenerator,
EmailSenderFactory emailSenderFactory,
NotificationSender notificationSender,
StoreRepository storeRepository,
LinkGenerator generator,
Logs logs)
: EventHostedServiceBase(eventAggregator, logs)
{
public UserManager<ApplicationUser> UserManager { get; } = userManager;
public CallbackGenerator CallbackGenerator { get; } = callbackGenerator;
protected override void SubscribeToEvents()
{
Subscribe<UserRegisteredEvent>();
Subscribe<UserApprovedEvent>();
Subscribe<UserConfirmedEmailEvent>();
Subscribe<UserPasswordResetRequestedEvent>();
Subscribe<UserInviteAcceptedEvent>();
Subscribe<UserEvent.Registered>();
Subscribe<UserEvent.Approved>();
Subscribe<UserEvent.ConfirmedEmail>();
Subscribe<UserEvent.PasswordResetRequested>();
Subscribe<UserEvent.InviteAccepted>();
}
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<ApplicationUser>(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);
}
}
}

View file

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

View file

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

View file

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

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

@ -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<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 StoreRemovedEvent(store));
}
await ctx.SaveChangesAsync();
events.ForEach(e => _eventAggregator.Publish(e));
}
public async Task<bool> 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<WebhookData[]> 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)
{

View file

@ -108,7 +108,7 @@ namespace BTCPayServer.Services
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();
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
@ -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
{