Files
btcpayserver/BTCPayServer/HostedServices/UserEventHostedService.cs
d11n e43b4ed540 Onboarding: Invite new users (#5714)
* Server Users: More precise message when inviting users

This lets the admin who invited a new user know whether or not an email has been sent. If the SMTP server hasn't been set up, they need to share the invite link with the user.

* Onboarding: Invite new users

- Separates the user self-registration and invite cases
- Adds invitation email for users created by the admin
- Adds invitation tokens to verify user was invited
- Adds handler action for invite links
- Refactors `UserEventHostedService`

* Remove duplicate status message from views that use the wizard layout

* Auto-approve users created by an admin

* Notify admins via email if a new account requires approval

* Update wording

* Fix update user error

* Fix redirect to email confirmation in invite action

* Fix precondition checks after signup

* Improve admin notification

Send notification only if the user does not require email confirmation or when they confirmed their email address. Rationale: We want to inform admins only about qualified users and not annoy them with bot registrations.

* Allow approval alongside resending confirm email

* Use user email in log messages instead of ID

* Prevent unnecessary notification after email confirmation

* Use ApplicationUser type explicitly

* Fix after rebase

* Refactoring: Do not subclass UserRegisteredEvent
2024-02-28 20:43:18 +09:00

142 lines
6.2 KiB
C#

using System;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Logging;
using BTCPayServer.Services;
using BTCPayServer.Services.Mails;
using BTCPayServer.Services.Notifications;
using BTCPayServer.Services.Notifications.Blobs;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Logging;
namespace BTCPayServer.HostedServices;
public class UserEventHostedService(
EventAggregator eventAggregator,
UserManager<ApplicationUser> userManager,
EmailSenderFactory emailSenderFactory,
NotificationSender notificationSender,
LinkGenerator generator,
Logs logs)
: EventHostedServiceBase(eventAggregator, logs)
{
protected override void SubscribeToEvents()
{
Subscribe<UserRegisteredEvent>();
Subscribe<UserApprovedEvent>();
Subscribe<UserConfirmedEmailEvent>();
Subscribe<UserPasswordResetRequestedEvent>();
}
protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken)
{
string code;
string callbackUrl;
Uri uri;
HostString host;
ApplicationUser user;
IEmailSender emailSender;
switch (evt)
{
case UserRegisteredEvent ev:
user = ev.User;
uri = ev.RequestUri;
host = new HostString(uri.Host, uri.Port);
// can be either a self-registration or by invite from another user
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 requiresApproval = user.RequiresApproval && !user.Approved;
var requiresEmailConfirmation = user.RequiresEmailConfirmation && !user.EmailConfirmed;
// log registration info
var newUserInfo = $"New {type} {user.Email} {info}";
Logs.PayServer.LogInformation(newUserInfo);
// send notification if the user does not require email confirmation.
// inform admins only about qualified users and not annoy them with bot registrations.
if (requiresApproval && !requiresEmailConfirmation)
{
await NotifyAdminsAboutUserRequiringApproval(user, uri, newUserInfo);
}
// set callback result and send email to user
emailSender = await emailSenderFactory.GetEmailSender();
if (isInvite)
{
code = await userManager.GenerateInvitationTokenAsync(user);
callbackUrl = generator.InvitationLink(user.Id, code, uri.Scheme, host, uri.PathAndQuery);
ev.CallbackUrlGenerated?.SetResult(new Uri(callbackUrl));
emailSender.SendInvitation(user.GetMailboxAddress(), callbackUrl);
}
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);
}
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));
Logs.PayServer.LogInformation("User {Email} requested a password reset", user.Email);
emailSender = await emailSenderFactory.GetEmailSender();
emailSender.SendResetPassword(user.GetMailboxAddress(), callbackUrl);
break;
case UserApprovedEvent approvedEvent:
user = approvedEvent.User;
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);
break;
case UserConfirmedEmailEvent confirmedEvent:
user = confirmedEvent.User;
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);
break;
}
}
private async Task NotifyAdminsAboutUserRequiringApproval(ApplicationUser user, Uri uri, string newUserInfo)
{
if (!user.RequiresApproval || user.Approved) return;
await notificationSender.SendNotification(new AdminScope(), new NewUserRequiresApprovalNotification(user));
var admins = await userManager.GetUsersInRoleAsync(Roles.ServerAdmin);
var host = new HostString(uri.Host, uri.Port);
var approvalLink = generator.UserDetailsLink(user.Id, uri.Scheme, host, uri.PathAndQuery);
var emailSender = await emailSenderFactory.GetEmailSender();
foreach (var admin in admins)
{
emailSender.SendNewUserInfo(admin.GetMailboxAddress(), newUserInfo, approvalLink);
}
}
}