mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-17 22:14:26 +01:00
Add invite and confirmation emails
This commit is contained in:
@@ -220,7 +220,7 @@ namespace BTCPayServer.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
var callbackUrl = await _callbackGenerator.ForEmailConfirmation(user, Request);
|
var callbackUrl = await _callbackGenerator.ForEmailConfirmation(user, Request);
|
||||||
(await _EmailSenderFactory.GetEmailSender()).SendEmailConfirmation(user.GetMailboxAddress(), callbackUrl);
|
_eventAggregator.Publish(new UserEvent.ConfirmationEmailRequested(user, callbackUrl));
|
||||||
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Verification email sent. Please check your email."].Value;
|
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Verification email sent. Please check your email."].Value;
|
||||||
return RedirectToAction(nameof(Index));
|
return RedirectToAction(nameof(Index));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -405,9 +405,7 @@ namespace BTCPayServer.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
var callbackUrl = await _callbackGenerator.ForEmailConfirmation(user, Request);
|
var callbackUrl = await _callbackGenerator.ForEmailConfirmation(user, Request);
|
||||||
|
_eventAggregator.Publish(new UserEvent.ConfirmationEmailRequested(user, callbackUrl));
|
||||||
(await _emailSenderFactory.GetEmailSender()).SendEmailConfirmation(user.GetMailboxAddress(), callbackUrl);
|
|
||||||
|
|
||||||
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Verification email sent"].Value;
|
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Verification email sent"].Value;
|
||||||
return RedirectToAction(nameof(ListUsers));
|
return RedirectToAction(nameof(ListUsers));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ public class UserEvent(ApplicationUser user)
|
|||||||
{
|
{
|
||||||
public string ResetLink { get; } = resetLink;
|
public string ResetLink { get; } = resetLink;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class ConfirmationEmailRequested(ApplicationUser user, string confirmLink) : UserEvent(user)
|
||||||
|
{
|
||||||
|
public string ConfirmLink { get; } = confirmLink;
|
||||||
|
}
|
||||||
|
|
||||||
public class Registered(ApplicationUser user, string approvalLink, string confirmationEmail) : UserEvent(user)
|
public class Registered(ApplicationUser user, string approvalLink, string confirmationEmail) : UserEvent(user)
|
||||||
{
|
{
|
||||||
public string ApprovalLink { get; } = approvalLink;
|
public string ApprovalLink { get; } = approvalLink;
|
||||||
|
|||||||
@@ -9,56 +9,16 @@ namespace BTCPayServer.Services
|
|||||||
{
|
{
|
||||||
private static string BODY_STYLE = "font-family: Open Sans, Helvetica Neue,Arial,sans-serif; font-color: #292929;";
|
private static string BODY_STYLE = "font-family: Open Sans, Helvetica Neue,Arial,sans-serif; font-color: #292929;";
|
||||||
private static string HEADER_HTML = "<h1 style='font-size:1.2rem'>BTCPay Server</h1><br/>";
|
private static string HEADER_HTML = "<h1 style='font-size:1.2rem'>BTCPay Server</h1><br/>";
|
||||||
private static string BUTTON_HTML = "<a href='{button_link}' type='submit' style='min-width: 2em;min-height: 20px;text-decoration-line: none;cursor: pointer;display: inline-block;font-weight: 400;color: #fff;text-align: center;vertical-align: middle;user-select: none;background-color: #51b13e;border-color: #51b13e;border: 1px solid transparent;padding: 0.375rem 0.75rem;font-size: 1rem;line-height: 1.5;border-radius: 0.25rem;transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;'>{button_description}</a>";
|
|
||||||
|
|
||||||
private static string CallToAction(string actionName, string actionLink)
|
|
||||||
{
|
|
||||||
var button = $"{BUTTON_HTML}".Replace("{button_description}", actionName, System.StringComparison.InvariantCulture);
|
|
||||||
return button.Replace("{button_link}", HtmlEncoder.Default.Encode(actionLink), System.StringComparison.InvariantCulture);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string CreateEmailBody(string body)
|
private static string CreateEmailBody(string body)
|
||||||
{
|
{
|
||||||
return $"<html><body style='{BODY_STYLE}'>{HEADER_HTML}{body}</body></html>";
|
return $"<html><body style='{BODY_STYLE}'>{HEADER_HTML}{body}</body></html>";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void SendEmailConfirmation(this IEmailSender emailSender, MailboxAddress address, string link)
|
|
||||||
{
|
|
||||||
emailSender.SendEmail(address, "Confirm your email", CreateEmailBody(
|
|
||||||
$"Please confirm your account.<br/><br/>{CallToAction("Confirm Email", link)}"));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void SendApprovalConfirmation(this IEmailSender emailSender, MailboxAddress address, string link)
|
|
||||||
{
|
|
||||||
emailSender.SendEmail(address, "Your account has been approved", CreateEmailBody(
|
|
||||||
$"Your account has been approved and you can now <a href='{HtmlEncoder.Default.Encode(link)}'>login here</a>."));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void SendInvitation(this IEmailSender emailSender, MailboxAddress address, string link)
|
|
||||||
{
|
|
||||||
emailSender.SendEmail(address, "Invitation", CreateEmailBody(
|
|
||||||
$"<p>Please complete your account setup by clicking <a href='{HtmlEncoder.Default.Encode(link)}'>this link</a>.</p><p>You can also use the BTCPay Server app and scan this QR code when connecting:</p>{GetQrCodeImg(link)}"));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void SendNewUserInfo(this IEmailSender emailSender, MailboxAddress address, string newUserInfo, string link)
|
|
||||||
{
|
|
||||||
emailSender.SendEmail(address, newUserInfo, CreateEmailBody(
|
|
||||||
$"{newUserInfo}. You can verify and approve the account here: <a href='{HtmlEncoder.Default.Encode(link)}'>User details</a>"));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void SendUserInviteAcceptedInfo(this IEmailSender emailSender, MailboxAddress address, string userInfo, string link)
|
public static void SendUserInviteAcceptedInfo(this IEmailSender emailSender, MailboxAddress address, string userInfo, string link)
|
||||||
{
|
{
|
||||||
emailSender.SendEmail(address, userInfo, CreateEmailBody(
|
emailSender.SendEmail(address, userInfo, CreateEmailBody(
|
||||||
$"{userInfo}. You can view the store users here: <a href='{HtmlEncoder.Default.Encode(link)}'>Store users</a>"));
|
$"{userInfo}. You can view the store users here: <a href='{HtmlEncoder.Default.Encode(link)}'>Store users</a>"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetQrCodeImg(string data)
|
|
||||||
{
|
|
||||||
using var qrGenerator = new QRCodeGenerator();
|
|
||||||
using var qrCodeData = qrGenerator.CreateQrCode(data, QRCodeGenerator.ECCLevel.Q);
|
|
||||||
using var qrCode = new Base64QRCode(qrCodeData);
|
|
||||||
var base64 = qrCode.GetGraphic(20);
|
|
||||||
return $"<img src='data:image/png;base64,{base64}' alt='{data}' width='320' height='320'/>";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -436,7 +436,6 @@ o.GetRequiredService<IEnumerable<IPaymentLinkExtension>>().ToDictionary(o => o.P
|
|||||||
services.AddSingleton<IHostedService, AppInventoryUpdaterHostedService>();
|
services.AddSingleton<IHostedService, AppInventoryUpdaterHostedService>();
|
||||||
services.AddSingleton<IHostedService, TransactionLabelMarkerHostedService>();
|
services.AddSingleton<IHostedService, TransactionLabelMarkerHostedService>();
|
||||||
services.AddSingleton<IHostedService, OnChainRateTrackerHostedService>();
|
services.AddSingleton<IHostedService, OnChainRateTrackerHostedService>();
|
||||||
services.AddSingleton<IHostedService, UserEventHostedService>();
|
|
||||||
services.AddSingleton<IHostedService, DynamicDnsHostedService>();
|
services.AddSingleton<IHostedService, DynamicDnsHostedService>();
|
||||||
services.AddSingleton<PaymentRequestStreamer>();
|
services.AddSingleton<PaymentRequestStreamer>();
|
||||||
services.AddSingleton<IHostedService>(s => s.GetRequiredService<PaymentRequestStreamer>());
|
services.AddSingleton<IHostedService>(s => s.GetRequiredService<PaymentRequestStreamer>());
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using BTCPayServer.Abstractions.Models;
|
using BTCPayServer.Abstractions.Models;
|
||||||
|
using BTCPayServer.Plugins.Emails.HostedServices;
|
||||||
using BTCPayServer.Plugins.Emails.Views;
|
using BTCPayServer.Plugins.Emails.Views;
|
||||||
using BTCPayServer.Plugins.Webhooks;
|
using BTCPayServer.Plugins.Webhooks;
|
||||||
using BTCPayServer.Services;
|
using BTCPayServer.Services;
|
||||||
@@ -19,6 +20,7 @@ public class EmailsPlugin : BaseBTCPayServerPlugin
|
|||||||
{
|
{
|
||||||
services.AddSingleton<IDefaultTranslationProvider, EmailsTranslationProvider>();
|
services.AddSingleton<IDefaultTranslationProvider, EmailsTranslationProvider>();
|
||||||
services.AddSingleton<IHostedService, StoreEmailRuleProcessorSender>();
|
services.AddSingleton<IHostedService, StoreEmailRuleProcessorSender>();
|
||||||
|
services.AddSingleton<IHostedService, UserEventHostedService>();
|
||||||
RegisterServerEmailTriggers(services);
|
RegisterServerEmailTriggers(services);
|
||||||
}
|
}
|
||||||
private static string BODY_STYLE = "font-family: Open Sans, Helvetica Neue,Arial,sans-serif; font-color: #292929;";
|
private static string BODY_STYLE = "font-family: Open Sans, Helvetica Neue,Arial,sans-serif; font-color: #292929;";
|
||||||
@@ -50,8 +52,67 @@ public class EmailsPlugin : BaseBTCPayServerPlugin
|
|||||||
Description = "Password Reset Requested",
|
Description = "Password Reset Requested",
|
||||||
};
|
};
|
||||||
vms.Add(vm);
|
vms.Add(vm);
|
||||||
|
|
||||||
|
vm = new EmailTriggerViewModel()
|
||||||
|
{
|
||||||
|
Trigger = ServerMailTriggers.EmailConfirm,
|
||||||
|
RecipientExample = "{User.MailboxAddress}",
|
||||||
|
SubjectExample = "Confirm your email",
|
||||||
|
BodyExample = CreateEmailBody($"Please confirm your account.<br/><br/>{CallToAction("Confirm Email", "{ConfirmLink}")}"),
|
||||||
|
PlaceHolders = new()
|
||||||
|
{
|
||||||
|
new ("{ConfirmLink}", "The link used to confirm the user's email address")
|
||||||
|
},
|
||||||
|
Description = "Confirm new user's email",
|
||||||
|
};
|
||||||
|
vms.Add(vm);
|
||||||
|
|
||||||
|
vm = new EmailTriggerViewModel()
|
||||||
|
{
|
||||||
|
Trigger = ServerMailTriggers.InvitePending,
|
||||||
|
RecipientExample = "{User.MailboxAddress}",
|
||||||
|
SubjectExample = "Invitation to join {Branding.ServerName}",
|
||||||
|
BodyExample = CreateEmailBody($"<p>Please complete your account setup by clicking <a href='{{InvitationLink}}'>this link</a>.</p><p>You can also use the BTCPay Server app and scan this QR code when connecting:</p>{{InvitationLinkQR}}"),
|
||||||
|
PlaceHolders = new()
|
||||||
|
{
|
||||||
|
new ("{InvitationLink}", "The link where the invited user can set up their account"),
|
||||||
|
new ("{InvitationLinkQR}", "The QR code representation of the invitation link")
|
||||||
|
},
|
||||||
|
Description = "User invitation email",
|
||||||
|
};
|
||||||
|
vms.Add(vm);
|
||||||
|
|
||||||
|
vm = new EmailTriggerViewModel()
|
||||||
|
{
|
||||||
|
Trigger = ServerMailTriggers.ApprovalConfirmed,
|
||||||
|
RecipientExample = "{User.MailboxAddress}",
|
||||||
|
SubjectExample = "Your account has been approved",
|
||||||
|
BodyExample = CreateEmailBody($"Your account has been approved and you can now.<br/><br/>{CallToAction("Login here", "{LoginLink}")}"),
|
||||||
|
PlaceHolders = new()
|
||||||
|
{
|
||||||
|
new ("{LoginLink}", "The link that the user can use to login"),
|
||||||
|
},
|
||||||
|
Description = "User account approved",
|
||||||
|
};
|
||||||
|
vms.Add(vm);
|
||||||
|
|
||||||
|
vm = new EmailTriggerViewModel()
|
||||||
|
{
|
||||||
|
Trigger = ServerMailTriggers.ApprovalRequest,
|
||||||
|
RecipientExample = "{Admin.MailboxAddresses}",
|
||||||
|
SubjectExample = "Approval request to access the server for {User.Email}",
|
||||||
|
BodyExample = CreateEmailBody($"A new user ({{User.MailboxAddress}}), is awaiting approval to access the server.<br/><br/>{CallToAction("Approve", "{ApprovalLink}")}"),
|
||||||
|
PlaceHolders = new()
|
||||||
|
{
|
||||||
|
new ("{ApprovalLink}", "The link that the admin needs to use to approve the user"),
|
||||||
|
},
|
||||||
|
Description = "Approval request to administrators",
|
||||||
|
};
|
||||||
|
vms.Add(vm);
|
||||||
|
|
||||||
var commonPlaceholders = new List<EmailTriggerViewModel.PlaceHolder>()
|
var commonPlaceholders = new List<EmailTriggerViewModel.PlaceHolder>()
|
||||||
{
|
{
|
||||||
|
new("{Admins.MailboxAddresses}", "The email addresses of the admins separated by a comma (eg. ,)"),
|
||||||
new("{User.Name}", "The name of the user (eg. John Doe)"),
|
new("{User.Name}", "The name of the user (eg. John Doe)"),
|
||||||
new("{User.Email}", "The email of the user (eg. john.doe@example.com)"),
|
new("{User.Email}", "The email of the user (eg. john.doe@example.com)"),
|
||||||
new("{User.MailboxAddress}", "The formatted mailbox address to use when sending an email. (eg. \"John Doe\" <john.doe@example.com>)"),
|
new("{User.MailboxAddress}", "The formatted mailbox address to use when sending an email. (eg. \"John Doe\" <john.doe@example.com>)"),
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Net.Mail;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
@@ -10,7 +12,7 @@ using Microsoft.Extensions.Logging;
|
|||||||
using MimeKit;
|
using MimeKit;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace BTCPayServer.Plugins.Emails;
|
namespace BTCPayServer.Plugins.Emails.HostedServices;
|
||||||
|
|
||||||
public interface ITriggerOwner
|
public interface ITriggerOwner
|
||||||
{
|
{
|
||||||
@@ -65,14 +67,30 @@ public class StoreEmailRuleProcessorSender(
|
|||||||
var subject = new TextTemplate(actionableRule.Subject ?? "");
|
var subject = new TextTemplate(actionableRule.Subject ?? "");
|
||||||
matchedContext.Recipients.AddRange(
|
matchedContext.Recipients.AddRange(
|
||||||
actionableRule.To
|
actionableRule.To
|
||||||
.Select(o =>
|
.SelectMany(o =>
|
||||||
{
|
{
|
||||||
if (!MailboxAddressValidator.TryParse(o, out var oo))
|
if (MailboxAddressValidator.TryParse(o, out var oo))
|
||||||
|
return new[] { oo };
|
||||||
|
|
||||||
|
var emails = new TextTemplate(o).Render(triggEvent.Model);
|
||||||
|
MailAddressCollection mailCollection = new();
|
||||||
|
try
|
||||||
{
|
{
|
||||||
MailboxAddressValidator.TryParse(new TextTemplate(o).Render(triggEvent.Model), out oo);
|
mailCollection.Add(emails);
|
||||||
}
|
}
|
||||||
|
catch (FormatException)
|
||||||
|
{
|
||||||
|
return Array.Empty<MailboxAddress>();
|
||||||
|
}
|
||||||
|
|
||||||
|
return mailCollection.Select(a =>
|
||||||
|
{
|
||||||
|
MailboxAddressValidator.TryParse(a.ToString(), out oo);
|
||||||
return oo;
|
return oo;
|
||||||
})
|
})
|
||||||
|
.Where(a => a != null)
|
||||||
|
.ToArray();
|
||||||
|
})
|
||||||
.Where(o => o != null)!);
|
.Where(o => o != null)!);
|
||||||
|
|
||||||
if (triggEvent.Owner is not null)
|
if (triggEvent.Owner is not null)
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
|
#nullable enable
|
||||||
|
using System.Linq;
|
||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using BTCPayServer.Events;
|
using BTCPayServer.Events;
|
||||||
|
using BTCPayServer.HostedServices;
|
||||||
using BTCPayServer.Logging;
|
using BTCPayServer.Logging;
|
||||||
using BTCPayServer.Plugins.Emails;
|
|
||||||
using BTCPayServer.Services;
|
using BTCPayServer.Services;
|
||||||
using BTCPayServer.Services.Mails;
|
using BTCPayServer.Services.Mails;
|
||||||
using BTCPayServer.Services.Notifications;
|
using BTCPayServer.Services.Notifications;
|
||||||
@@ -13,13 +15,13 @@ using BTCPayServer.Services.Stores;
|
|||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
using QRCoder;
|
||||||
|
|
||||||
namespace BTCPayServer.HostedServices;
|
namespace BTCPayServer.Plugins.Emails.HostedServices;
|
||||||
|
|
||||||
public class UserEventHostedService(
|
public class UserEventHostedService(
|
||||||
EventAggregator eventAggregator,
|
EventAggregator eventAggregator,
|
||||||
UserManager<ApplicationUser> userManager,
|
UserManager<ApplicationUser> userManager,
|
||||||
CallbackGenerator callbackGenerator,
|
|
||||||
ISettingsAccessor<ServerSettings> serverSettings,
|
ISettingsAccessor<ServerSettings> serverSettings,
|
||||||
EmailSenderFactory emailSenderFactory,
|
EmailSenderFactory emailSenderFactory,
|
||||||
NotificationSender notificationSender,
|
NotificationSender notificationSender,
|
||||||
@@ -28,62 +30,64 @@ public class UserEventHostedService(
|
|||||||
: EventHostedServiceBase(eventAggregator, logs)
|
: EventHostedServiceBase(eventAggregator, logs)
|
||||||
{
|
{
|
||||||
public UserManager<ApplicationUser> UserManager { get; } = userManager;
|
public UserManager<ApplicationUser> UserManager { get; } = userManager;
|
||||||
public CallbackGenerator CallbackGenerator { get; } = callbackGenerator;
|
|
||||||
|
|
||||||
protected override void SubscribeToEvents()
|
protected override void SubscribeToEvents()
|
||||||
{
|
{
|
||||||
Subscribe<UserEvent.Registered>();
|
SubscribeAny<UserEvent>();
|
||||||
Subscribe<UserEvent.Invited>();
|
}
|
||||||
Subscribe<UserEvent.Approved>();
|
|
||||||
Subscribe<UserEvent.ConfirmedEmail>();
|
public static string GetQrCodeImg(string data)
|
||||||
Subscribe<UserEvent.PasswordResetRequested>();
|
{
|
||||||
Subscribe<UserEvent.InviteAccepted>();
|
using var qrGenerator = new QRCodeGenerator();
|
||||||
|
using var qrCodeData = qrGenerator.CreateQrCode(data, QRCodeGenerator.ECCLevel.Q);
|
||||||
|
using var qrCode = new Base64QRCode(qrCodeData);
|
||||||
|
var base64 = qrCode.GetGraphic(20);
|
||||||
|
return $"<img src='data:image/png;base64,{base64}' alt='{data}' width='320' height='320'/>";
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken)
|
protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
ApplicationUser user = (evt as UserEvent).User;
|
var user = (evt as UserEvent)?.User;
|
||||||
IEmailSender emailSender;
|
if (user is null) return;
|
||||||
switch (evt)
|
switch (evt)
|
||||||
{
|
{
|
||||||
case UserEvent.Registered ev:
|
case UserEvent.Registered ev:
|
||||||
// can be either a self-registration or by invite from another user
|
var requiresApproval = user is { RequiresApproval: true, Approved: false };
|
||||||
var type = await UserManager.IsInRoleAsync(user, Roles.ServerAdmin) ? "admin" : "user";
|
var requiresEmailConfirmation = user is { RequiresEmailConfirmation: true, EmailConfirmed: false };
|
||||||
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;
|
|
||||||
|
|
||||||
// log registration info
|
|
||||||
var newUserInfo = $"New {type} {user.Email} {info}";
|
|
||||||
Logs.PayServer.LogInformation(newUserInfo);
|
|
||||||
|
|
||||||
// send notification if the user does not require email confirmation.
|
// send notification if the user does not require email confirmation.
|
||||||
// inform admins only about qualified users and not annoy them with bot registrations.
|
// inform admins only about qualified users and not annoy them with bot registrations.
|
||||||
if (requiresApproval && !requiresEmailConfirmation)
|
if (requiresApproval && !requiresEmailConfirmation)
|
||||||
{
|
{
|
||||||
await NotifyAdminsAboutUserRequiringApproval(user, ev.ApprovalLink, newUserInfo);
|
await NotifyAdminsAboutUserRequiringApproval(user, ev.ApprovalLink);
|
||||||
}
|
}
|
||||||
|
|
||||||
// set callback result and send email to user
|
// set callback result and send email to user
|
||||||
emailSender = await emailSenderFactory.GetEmailSender();
|
|
||||||
if (ev is UserEvent.Invited invited)
|
if (ev is UserEvent.Invited invited)
|
||||||
{
|
{
|
||||||
if (invited.SendInvitationEmail)
|
if (invited.SendInvitationEmail)
|
||||||
emailSender.SendInvitation(user.GetMailboxAddress(), invited.InvitationLink);
|
EventAggregator.Publish(await CreateTriggerEvent(ServerMailTriggers.InvitePending,
|
||||||
|
new JObject()
|
||||||
|
{
|
||||||
|
["InvitationLink"] = HtmlEncoder.Default.Encode(invited.InvitationLink),
|
||||||
|
["InvitationLinkQR"] = GetQrCodeImg(invited.InvitationLink)
|
||||||
|
}, user));
|
||||||
}
|
}
|
||||||
else if (requiresEmailConfirmation)
|
else if (requiresEmailConfirmation)
|
||||||
{
|
{
|
||||||
emailSender.SendEmailConfirmation(user.GetMailboxAddress(), ev.ConfirmationEmailLink);
|
EventAggregator.Publish(new UserEvent.ConfirmationEmailRequested(user, ev.ConfirmationEmailLink));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case UserEvent.ConfirmationEmailRequested confReq:
|
||||||
|
EventAggregator.Publish(await CreateTriggerEvent(ServerMailTriggers.EmailConfirm,
|
||||||
|
new JObject()
|
||||||
|
{
|
||||||
|
["ConfirmLink"] = HtmlEncoder.Default.Encode(confReq.ConfirmLink)
|
||||||
|
}, user));
|
||||||
|
break;
|
||||||
|
|
||||||
case UserEvent.PasswordResetRequested pwResetEvent:
|
case UserEvent.PasswordResetRequested pwResetEvent:
|
||||||
EventAggregator.Publish(CreateTriggerEvent(ServerMailTriggers.PasswordReset,
|
EventAggregator.Publish(await CreateTriggerEvent(ServerMailTriggers.PasswordReset,
|
||||||
new JObject()
|
new JObject()
|
||||||
{
|
{
|
||||||
["ResetLink"] = HtmlEncoder.Default.Encode(pwResetEvent.ResetLink)
|
["ResetLink"] = HtmlEncoder.Default.Encode(pwResetEvent.ResetLink)
|
||||||
@@ -92,15 +96,16 @@ public class UserEventHostedService(
|
|||||||
|
|
||||||
case UserEvent.Approved approvedEvent:
|
case UserEvent.Approved approvedEvent:
|
||||||
if (!user.Approved) break;
|
if (!user.Approved) break;
|
||||||
emailSender = await emailSenderFactory.GetEmailSender();
|
EventAggregator.Publish(await CreateTriggerEvent(ServerMailTriggers.ApprovalConfirmed,
|
||||||
emailSender.SendApprovalConfirmation(user.GetMailboxAddress(), approvedEvent.LoginLink);
|
new JObject()
|
||||||
|
{
|
||||||
|
["LoginLink"] = approvedEvent.LoginLink
|
||||||
|
}, user));
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case UserEvent.ConfirmedEmail confirmedEvent:
|
case UserEvent.ConfirmedEmail confirmedEvent when user is { RequiresApproval: true, Approved: false, EmailConfirmed: true }:
|
||||||
if (!user.EmailConfirmed) break;
|
await NotifyAdminsAboutUserRequiringApproval(user, confirmedEvent.ApprovalLink);
|
||||||
var confirmedUserInfo = $"User {user.Email} confirmed their email address";
|
|
||||||
Logs.PayServer.LogInformation(confirmedUserInfo);
|
|
||||||
await NotifyAdminsAboutUserRequiringApproval(user, confirmedEvent.ApprovalLink, confirmedUserInfo);
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case UserEvent.InviteAccepted inviteAcceptedEvent:
|
case UserEvent.InviteAccepted inviteAcceptedEvent:
|
||||||
@@ -110,8 +115,24 @@ public class UserEventHostedService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private TriggerEvent CreateTriggerEvent(string trigger, JObject model, ApplicationUser user)
|
private async Task NotifyAdminsAboutUserRequiringApproval(ApplicationUser user, string approvalLink)
|
||||||
{
|
{
|
||||||
|
await notificationSender.SendNotification(new AdminScope(), new NewUserRequiresApprovalNotification(user));
|
||||||
|
EventAggregator.Publish(await CreateTriggerEvent(ServerMailTriggers.ApprovalRequest,
|
||||||
|
new JObject()
|
||||||
|
{
|
||||||
|
["ApprovalLink"] = approvalLink
|
||||||
|
}, user));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<TriggerEvent> CreateTriggerEvent(string trigger, JObject model, ApplicationUser user)
|
||||||
|
{
|
||||||
|
var admins = await UserManager.GetUsersInRoleAsync(Roles.ServerAdmin);
|
||||||
|
var adminMailboxes = string.Join(", ", admins.Select(a => a.GetMailboxAddress().ToString()).ToArray());
|
||||||
|
model["Admins"] = new JObject()
|
||||||
|
{
|
||||||
|
["MailboxAddresses"] = adminMailboxes,
|
||||||
|
};
|
||||||
model["User"] = new JObject()
|
model["User"] = new JObject()
|
||||||
{
|
{
|
||||||
["Name"] = user.UserName,
|
["Name"] = user.UserName,
|
||||||
@@ -126,21 +147,6 @@ public class UserEventHostedService(
|
|||||||
var evt = new TriggerEvent(null, trigger, model, null);
|
var evt = new TriggerEvent(null, trigger, model, null);
|
||||||
return evt;
|
return evt;
|
||||||
}
|
}
|
||||||
|
|
||||||
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 emailSender = await emailSenderFactory.GetEmailSender();
|
|
||||||
foreach (var admin in admins)
|
|
||||||
{
|
|
||||||
emailSender.SendNewUserInfo(admin.GetMailboxAddress(), newUserInfo, approvalLink);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task NotifyAboutUserAcceptingInvite(ApplicationUser user, string storeUsersLink)
|
private async Task NotifyAboutUserAcceptingInvite(ApplicationUser user, string storeUsersLink)
|
||||||
{
|
{
|
||||||
var stores = await storeRepository.GetStoresByUserId(user.Id);
|
var stores = await storeRepository.GetStoresByUserId(user.Id);
|
||||||
@@ -8,4 +8,5 @@ public class ServerMailTriggers
|
|||||||
public const string ApprovalConfirmed = "SRV-ApprovalConfirmed";
|
public const string ApprovalConfirmed = "SRV-ApprovalConfirmed";
|
||||||
public const string ApprovalPending = "SRV-ApprovalPending";
|
public const string ApprovalPending = "SRV-ApprovalPending";
|
||||||
public const string EmailConfirm = "SRV-EmailConfirmation";
|
public const string EmailConfirm = "SRV-EmailConfirmation";
|
||||||
|
public const string ApprovalRequest = "SRV-ApprovalRequest";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,18 @@ namespace BTCPayServer.Plugins.Emails.Views;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class EmailTriggerViewModel
|
public class EmailTriggerViewModel
|
||||||
{
|
{
|
||||||
public string Trigger { get; set; }
|
public class Default
|
||||||
public string Description { get; set; }
|
{
|
||||||
public string SubjectExample { get; set; }
|
public string SubjectExample { get; set; }
|
||||||
public string BodyExample { get; set; }
|
public string BodyExample { get; set; }
|
||||||
public bool CanIncludeCustomerEmail { get; set; }
|
public bool CanIncludeCustomerEmail { get; set; }
|
||||||
|
public string RecipientExample { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Trigger { get; set; }
|
||||||
|
|
||||||
|
public string Description { get; set; }
|
||||||
|
|
||||||
|
|
||||||
public class PlaceHolder(string name, string description)
|
public class PlaceHolder(string name, string description)
|
||||||
{
|
{
|
||||||
@@ -21,5 +28,4 @@ public class EmailTriggerViewModel
|
|||||||
|
|
||||||
public List<PlaceHolder> PlaceHolders { get; set; } = new();
|
public List<PlaceHolder> PlaceHolders { get; set; } = new();
|
||||||
public bool ServerTrigger { get; set; }
|
public bool ServerTrigger { get; set; }
|
||||||
public string RecipientExample { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,32 +152,20 @@
|
|||||||
const bodyTextarea = document.querySelector('.email-rule-body');
|
const bodyTextarea = document.querySelector('.email-rule-body');
|
||||||
const placeholdersTd = document.querySelector('#placeholders');
|
const placeholdersTd = document.querySelector('#placeholders');
|
||||||
|
|
||||||
const isEmptyOrDefault = (value, type) => {
|
|
||||||
const val = value.replace(/<.*?>/gi, '').trim();
|
|
||||||
if (!val) return true;
|
|
||||||
return Object.values(triggersByType).some(t => t[type] === val);
|
|
||||||
};
|
|
||||||
|
|
||||||
function applyTemplate() {
|
function applyTemplate() {
|
||||||
const selectedTrigger = triggerSelect.value;
|
const selectedTrigger = triggerSelect.value;
|
||||||
console.log(selectedTrigger);
|
|
||||||
if (triggersByType[selectedTrigger]) {
|
if (triggersByType[selectedTrigger]) {
|
||||||
if (isEmptyOrDefault(subjectInput.value, 'subjectExample')) {
|
|
||||||
subjectInput.value = triggersByType[selectedTrigger].subjectExample;
|
subjectInput.value = triggersByType[selectedTrigger].subjectExample;
|
||||||
}
|
|
||||||
if (isEmptyOrDefault(recipientInput.value, 'recipientExample')) {
|
|
||||||
recipientInput.value = triggersByType[selectedTrigger].recipientExample;
|
recipientInput.value = triggersByType[selectedTrigger].recipientExample;
|
||||||
}
|
|
||||||
var body = triggersByType[selectedTrigger].bodyExample;
|
var body = triggersByType[selectedTrigger].bodyExample;
|
||||||
|
|
||||||
if (isEmptyOrDefault(bodyTextarea.value, 'bodyExample')) {
|
|
||||||
if ($(bodyTextarea).summernote) {
|
if ($(bodyTextarea).summernote) {
|
||||||
|
console.log(body);
|
||||||
$(bodyTextarea).summernote('reset');
|
$(bodyTextarea).summernote('reset');
|
||||||
$(bodyTextarea).summernote('code', body.replace(/\n/g, '<br/>'));
|
$(bodyTextarea).summernote('code', body.replace(/\n/g, '<br/>'));
|
||||||
} else {
|
} else {
|
||||||
bodyTextarea.value = body;
|
bodyTextarea.value = body;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
placeholdersTd.innerHTML = '';
|
placeholdersTd.innerHTML = '';
|
||||||
triggersByType[selectedTrigger].placeHolders.forEach(p => {
|
triggersByType[selectedTrigger].placeHolders.forEach(p => {
|
||||||
@@ -213,7 +201,10 @@
|
|||||||
|
|
||||||
// Apply template on page load if a trigger is selected
|
// Apply template on page load if a trigger is selected
|
||||||
if (triggerSelect.value) {
|
if (triggerSelect.value) {
|
||||||
|
if (@Safe.Json(!isEdit))
|
||||||
|
{
|
||||||
applyTemplate();
|
applyTemplate();
|
||||||
|
}
|
||||||
toggleCustomerEmailVisibility();
|
toggleCustomerEmailVisibility();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using BTCPayServer.Client.Models;
|
|||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using BTCPayServer.HostedServices;
|
using BTCPayServer.HostedServices;
|
||||||
using BTCPayServer.Plugins.Emails;
|
using BTCPayServer.Plugins.Emails;
|
||||||
|
using BTCPayServer.Plugins.Emails.HostedServices;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using StoreData = BTCPayServer.Data.StoreData;
|
using StoreData = BTCPayServer.Data.StoreData;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using BTCPayServer.Client.Models;
|
|||||||
using BTCPayServer.Controllers.Greenfield;
|
using BTCPayServer.Controllers.Greenfield;
|
||||||
using BTCPayServer.Events;
|
using BTCPayServer.Events;
|
||||||
using BTCPayServer.Plugins.Emails;
|
using BTCPayServer.Plugins.Emails;
|
||||||
|
using BTCPayServer.Plugins.Emails.HostedServices;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using BTCPayServer.Client.Models;
|
|||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using BTCPayServer.HostedServices;
|
using BTCPayServer.HostedServices;
|
||||||
using BTCPayServer.Plugins.Emails;
|
using BTCPayServer.Plugins.Emails;
|
||||||
|
using BTCPayServer.Plugins.Emails.HostedServices;
|
||||||
using BTCPayServer.Services.PaymentRequests;
|
using BTCPayServer.Services.PaymentRequests;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Threading.Tasks;
|
|||||||
using BTCPayServer.Client.Models;
|
using BTCPayServer.Client.Models;
|
||||||
using BTCPayServer.HostedServices;
|
using BTCPayServer.HostedServices;
|
||||||
using BTCPayServer.Plugins.Emails;
|
using BTCPayServer.Plugins.Emails;
|
||||||
|
using BTCPayServer.Plugins.Emails.HostedServices;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using StoreData = BTCPayServer.Data.StoreData;
|
using StoreData = BTCPayServer.Data.StoreData;
|
||||||
|
|||||||
@@ -48,10 +48,7 @@ namespace BTCPayServer.Services.Notifications
|
|||||||
}
|
}
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
foreach (string user in users)
|
_notificationManager.InvalidateNotificationCache(users);
|
||||||
{
|
|
||||||
_notificationManager.InvalidateNotificationCache(user);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public BaseNotification GetBaseNotification(NotificationData notificationData)
|
public BaseNotification GetBaseNotification(NotificationData notificationData)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ using MimeKit;
|
|||||||
namespace BTCPayServer
|
namespace BTCPayServer
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Validate address in the format "Firstname Lastname <blah@example.com>" See rfc822
|
/// Validate address in the format "Firstname Lastname <blah@example.com>" See rfc5322
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class MailboxAddressValidator
|
public class MailboxAddressValidator
|
||||||
{
|
{
|
||||||
@@ -25,7 +25,7 @@ namespace BTCPayServer
|
|||||||
public static MailboxAddress Parse(string? str)
|
public static MailboxAddress Parse(string? str)
|
||||||
{
|
{
|
||||||
if (!TryParse(str, out var mb))
|
if (!TryParse(str, out var mb))
|
||||||
throw new FormatException("Invalid mailbox address (rfc822)");
|
throw new FormatException("Invalid mailbox address (rfc5322)");
|
||||||
return mb;
|
return mb;
|
||||||
}
|
}
|
||||||
public static bool TryParse(string? str, [MaybeNullWhen(false)] out MailboxAddress mailboxAddress)
|
public static bool TryParse(string? str, [MaybeNullWhen(false)] out MailboxAddress mailboxAddress)
|
||||||
|
|||||||
Reference in New Issue
Block a user