Changed email tokens to be generated and validated in EmailTemplateService
This commit is contained in:
parent
c0afba7554
commit
332abae312
|
@ -7,13 +7,11 @@
|
||||||
@using System.Net
|
@using System.Net
|
||||||
@using Microsoft.AspNetCore.Identity.UI.Services
|
@using Microsoft.AspNetCore.Identity.UI.Services
|
||||||
@using Microsoft.EntityFrameworkCore
|
@using Microsoft.EntityFrameworkCore
|
||||||
@using Microsoft.Extensions.Caching.Distributed
|
|
||||||
@using Wave.Services
|
@using Wave.Services
|
||||||
|
|
||||||
@inject ILogger<EmailSignup> Logger
|
@inject ILogger<EmailSignup> Logger
|
||||||
@inject IStringLocalizer<EmailSignup> Localizer
|
@inject IStringLocalizer<EmailSignup> Localizer
|
||||||
@inject IDbContextFactory<ApplicationDbContext> ContextFactory
|
@inject IDbContextFactory<ApplicationDbContext> ContextFactory
|
||||||
@inject IDistributedCache TokenCache
|
|
||||||
@inject IOptions<Features> Features
|
@inject IOptions<Features> Features
|
||||||
@inject IOptions<Customization> Customizations
|
@inject IOptions<Customization> Customizations
|
||||||
@inject NavigationManager Navigation
|
@inject NavigationManager Navigation
|
||||||
|
@ -60,32 +58,28 @@
|
||||||
|
|
||||||
private string Message { get; set; } = string.Empty;
|
private string Message { get; set; } = string.Empty;
|
||||||
|
|
||||||
protected override void OnInitialized() {
|
protected override async Task OnInitializedAsync() {
|
||||||
if (Features.Value.EmailSubscriptions is not true)
|
if (Features.Value.EmailSubscriptions is not true)
|
||||||
throw new ApplicationException("Email subscriptions not enabled.");
|
throw new ApplicationException("Email subscriptions not enabled.");
|
||||||
|
|
||||||
if (Id is null || Token is null) return;
|
if (Id is null || Token is null) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
string cacheKey = "subscribe-" + Id;
|
var id = await TemplateService.ValidateTokensAsync(Id, Token);
|
||||||
byte[]? tokenInCache = TokenCache.Get(cacheKey);
|
|
||||||
|
|
||||||
if (tokenInCache is null || Token != Convert.ToBase64String(tokenInCache)) {
|
if (id is null) {
|
||||||
Message = Localizer["Failure_Message"];
|
Message = Localizer["Failure_Message"];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var realId = new Guid(Convert.FromBase64String(Id));
|
await using var context = await ContextFactory.CreateDbContextAsync();
|
||||||
using var context = ContextFactory.CreateDbContext();
|
var subscriber = context.Set<EmailSubscriber>().IgnoreQueryFilters().FirstOrDefault(s => s.Id == id);
|
||||||
var subscriber = context.Set<EmailSubscriber>().IgnoreQueryFilters().FirstOrDefault(s => s.Id == realId);
|
|
||||||
if (subscriber is null) {
|
if (subscriber is null) {
|
||||||
Message = Localizer["Failure_Message"];
|
Message = Localizer["Failure_Message"];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
subscriber.Unsubscribed = false;
|
subscriber.Unsubscribed = false;
|
||||||
context.SaveChanges();
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
TokenCache.Remove(cacheKey);
|
|
||||||
Message = Localizer["Success_Message"];
|
Message = Localizer["Success_Message"];
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.LogError(ex, "Error trying to confirm subscriber.");
|
Logger.LogError(ex, "Error trying to confirm subscriber.");
|
||||||
|
@ -97,43 +91,41 @@
|
||||||
if (Features.Value.EmailSubscriptions is not true)
|
if (Features.Value.EmailSubscriptions is not true)
|
||||||
throw new ApplicationException("Email subscriptions not enabled.");
|
throw new ApplicationException("Email subscriptions not enabled.");
|
||||||
|
|
||||||
Message = Localizer["Submit_Message"];
|
try {
|
||||||
await using var context = await ContextFactory.CreateDbContextAsync();
|
Message = Localizer["Submit_Message"];
|
||||||
|
await using var context = await ContextFactory.CreateDbContextAsync();
|
||||||
|
|
||||||
var subscriber = context.Set<EmailSubscriber>().IgnoreQueryFilters().FirstOrDefault(s => s.Email == Model.Email);
|
var subscriber = context.Set<EmailSubscriber>().IgnoreQueryFilters().FirstOrDefault(s => s.Email == Model.Email);
|
||||||
if (subscriber?.Unsubscribed is false) return;
|
if (subscriber?.Unsubscribed is false) return;
|
||||||
|
|
||||||
subscriber ??= new EmailSubscriber {
|
subscriber ??= new EmailSubscriber {
|
||||||
Email = Model.Email.Trim(),
|
Email = Model.Email.Trim(),
|
||||||
Unsubscribed = true
|
Unsubscribed = true
|
||||||
};
|
};
|
||||||
subscriber.Name = Model.Name;
|
subscriber.Name = Model.Name;
|
||||||
context.Update(subscriber);
|
context.Update(subscriber);
|
||||||
await context.SaveChangesAsync();
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
if (subscriber.Unsubscribed) {
|
if (subscriber.Unsubscribed) {
|
||||||
string id = Convert.ToBase64String(subscriber.Id.ToByteArray());
|
(string id, string token) = await TemplateService.CreateConfirmTokensAsync(subscriber.Id);
|
||||||
string token = Convert.ToBase64String(Guid.NewGuid().ToByteArray());
|
|
||||||
|
|
||||||
await TokenCache.SetAsync("subscribe-" + id,
|
string confirmLink = Navigation.ToAbsoluteUri(
|
||||||
Convert.FromBase64String(token),
|
$"/Email/Confirm?user={WebUtility.UrlEncode(id)}&token={WebUtility.UrlEncode(token)}").AbsoluteUri;
|
||||||
new DistributedCacheEntryOptions {
|
|
||||||
AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(1)
|
|
||||||
});
|
|
||||||
|
|
||||||
string confirmLink = Navigation.ToAbsoluteUri(
|
var customization = Customizations.Value;
|
||||||
$"/Email/Confirm?user={WebUtility.UrlEncode(id)}&token={WebUtility.UrlEncode(token)}").AbsoluteUri;
|
string body = TemplateService.Default(
|
||||||
|
Navigation.BaseUri,
|
||||||
var customization = Customizations.Value;
|
!string.IsNullOrWhiteSpace(customization.LogoLink) ?
|
||||||
string body = TemplateService.Default(
|
customization.LogoLink :
|
||||||
Navigation.BaseUri,
|
Navigation.ToAbsoluteUri("/img/logo.png").AbsoluteUri,
|
||||||
!string.IsNullOrWhiteSpace(customization.LogoLink) ?
|
Localizer["ConfirmEmailTitle"],
|
||||||
customization.LogoLink :
|
string.Format(Localizer["ConfirmEmailBody"], customization.AppName) +
|
||||||
Navigation.ToAbsoluteUri("/img/logo.png").AbsoluteUri,
|
$"""<p style="text-align: center"><a href="{confirmLink}">{Localizer["Submit"]}</a></p>""");
|
||||||
Localizer["ConfirmEmailTitle"],
|
await EmailSender.SendEmailAsync(subscriber.Email, Localizer["ConfirmEmailSubject"], body);
|
||||||
string.Format(Localizer["ConfirmEmailBody"], customization.AppName) +
|
}
|
||||||
$"""<p style="text-align: center"><a href="{confirmLink}">{Localizer["Submit"]}</a></p>""");
|
} catch (Exception ex) {
|
||||||
await EmailSender.SendEmailAsync(subscriber.Email, Localizer["ConfirmEmailSubject"], body);
|
Logger.LogError(ex, "Failed to create subscriber/send confirmation mail.");
|
||||||
|
Message = Localizer["Failure_Message"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,46 @@
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using Microsoft.Extensions.Caching.Distributed;
|
||||||
using Mjml.Net;
|
using Mjml.Net;
|
||||||
|
|
||||||
namespace Wave.Services;
|
namespace Wave.Services;
|
||||||
|
|
||||||
public partial class EmailTemplateService(ILogger<EmailTemplateService> logger) {
|
public partial class EmailTemplateService(ILogger<EmailTemplateService> logger, IDistributedCache tokenCache) {
|
||||||
public enum Constants {
|
public enum Constants {
|
||||||
BrowserLink, HomeLink, ContentLogo, ContentTitle, ContentBody, EmailUnsubscribeLink
|
BrowserLink, HomeLink, ContentLogo, ContentTitle, ContentBody, EmailUnsubscribeLink
|
||||||
}
|
}
|
||||||
|
|
||||||
private ILogger<EmailTemplateService> Logger { get; } = logger;
|
private ILogger<EmailTemplateService> Logger { get; } = logger;
|
||||||
private IMjmlRenderer Renderer { get; } = new MjmlRenderer();
|
private IMjmlRenderer Renderer { get; } = new MjmlRenderer();
|
||||||
|
private IDistributedCache TokenCache { get; } = tokenCache;
|
||||||
|
|
||||||
private Regex TokenMatcher { get; } = MyRegex();
|
private Regex TokenMatcher { get; } = MyRegex();
|
||||||
|
|
||||||
|
public async Task<(string user, string token)> CreateConfirmTokensAsync(Guid subscriberId) {
|
||||||
|
string user = Convert.ToBase64String(subscriberId.ToByteArray());
|
||||||
|
string token = Convert.ToBase64String(Guid.NewGuid().ToByteArray());
|
||||||
|
string cacheKey = "subscribe-" + user;
|
||||||
|
|
||||||
|
await TokenCache.SetAsync(cacheKey,
|
||||||
|
Convert.FromBase64String(token),
|
||||||
|
new DistributedCacheEntryOptions {
|
||||||
|
AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(1)
|
||||||
|
});
|
||||||
|
|
||||||
|
return (user, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Guid?> ValidateTokensAsync(string user, string token) {
|
||||||
|
string cacheKey = "subscribe-" + user;
|
||||||
|
byte[]? tokenInCache = await TokenCache.GetAsync(cacheKey);
|
||||||
|
|
||||||
|
if (tokenInCache is null || token != Convert.ToBase64String(tokenInCache))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
await TokenCache.RemoveAsync(cacheKey);
|
||||||
|
return new Guid(Convert.FromBase64String(user));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public string Default(string url, string logoLink, string title, string body) {
|
public string Default(string url, string logoLink, string title, string body) {
|
||||||
return Process("default", new Dictionary<Constants, object?> {
|
return Process("default", new Dictionary<Constants, object?> {
|
||||||
{Constants.HomeLink, url},
|
{Constants.HomeLink, url},
|
||||||
|
|
Loading…
Reference in a new issue