Changed email tokens to be generated and validated in EmailTemplateService

This commit is contained in:
Mia Rose Winter 2024-02-13 13:38:57 +01:00
parent c0afba7554
commit 332abae312
Signed by: miawinter
GPG key ID: 4B6F6A83178F595E
2 changed files with 65 additions and 45 deletions

View file

@ -7,13 +7,11 @@
@using System.Net
@using Microsoft.AspNetCore.Identity.UI.Services
@using Microsoft.EntityFrameworkCore
@using Microsoft.Extensions.Caching.Distributed
@using Wave.Services
@inject ILogger<EmailSignup> Logger
@inject IStringLocalizer<EmailSignup> Localizer
@inject IDbContextFactory<ApplicationDbContext> ContextFactory
@inject IDistributedCache TokenCache
@inject IOptions<Features> Features
@inject IOptions<Customization> Customizations
@inject NavigationManager Navigation
@ -60,32 +58,28 @@
private string Message { get; set; } = string.Empty;
protected override void OnInitialized() {
protected override async Task OnInitializedAsync() {
if (Features.Value.EmailSubscriptions is not true)
throw new ApplicationException("Email subscriptions not enabled.");
if (Id is null || Token is null) return;
try {
string cacheKey = "subscribe-" + Id;
byte[]? tokenInCache = TokenCache.Get(cacheKey);
var id = await TemplateService.ValidateTokensAsync(Id, Token);
if (tokenInCache is null || Token != Convert.ToBase64String(tokenInCache)) {
if (id is null) {
Message = Localizer["Failure_Message"];
return;
}
var realId = new Guid(Convert.FromBase64String(Id));
using var context = ContextFactory.CreateDbContext();
var subscriber = context.Set<EmailSubscriber>().IgnoreQueryFilters().FirstOrDefault(s => s.Id == realId);
await using var context = await ContextFactory.CreateDbContextAsync();
var subscriber = context.Set<EmailSubscriber>().IgnoreQueryFilters().FirstOrDefault(s => s.Id == id);
if (subscriber is null) {
Message = Localizer["Failure_Message"];
return;
}
subscriber.Unsubscribed = false;
context.SaveChanges();
await context.SaveChangesAsync();
TokenCache.Remove(cacheKey);
Message = Localizer["Success_Message"];
} catch (Exception ex) {
Logger.LogError(ex, "Error trying to confirm subscriber.");
@ -97,43 +91,41 @@
if (Features.Value.EmailSubscriptions is not true)
throw new ApplicationException("Email subscriptions not enabled.");
Message = Localizer["Submit_Message"];
await using var context = await ContextFactory.CreateDbContextAsync();
try {
Message = Localizer["Submit_Message"];
await using var context = await ContextFactory.CreateDbContextAsync();
var subscriber = context.Set<EmailSubscriber>().IgnoreQueryFilters().FirstOrDefault(s => s.Email == Model.Email);
if (subscriber?.Unsubscribed is false) return;
var subscriber = context.Set<EmailSubscriber>().IgnoreQueryFilters().FirstOrDefault(s => s.Email == Model.Email);
if (subscriber?.Unsubscribed is false) return;
subscriber ??= new EmailSubscriber {
Email = Model.Email.Trim(),
Unsubscribed = true
};
subscriber.Name = Model.Name;
context.Update(subscriber);
await context.SaveChangesAsync();
subscriber ??= new EmailSubscriber {
Email = Model.Email.Trim(),
Unsubscribed = true
};
subscriber.Name = Model.Name;
context.Update(subscriber);
await context.SaveChangesAsync();
if (subscriber.Unsubscribed) {
string id = Convert.ToBase64String(subscriber.Id.ToByteArray());
string token = Convert.ToBase64String(Guid.NewGuid().ToByteArray());
if (subscriber.Unsubscribed) {
(string id, string token) = await TemplateService.CreateConfirmTokensAsync(subscriber.Id);
await TokenCache.SetAsync("subscribe-" + id,
Convert.FromBase64String(token),
new DistributedCacheEntryOptions {
AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(1)
});
string confirmLink = Navigation.ToAbsoluteUri(
$"/Email/Confirm?user={WebUtility.UrlEncode(id)}&token={WebUtility.UrlEncode(token)}").AbsoluteUri;
string confirmLink = Navigation.ToAbsoluteUri(
$"/Email/Confirm?user={WebUtility.UrlEncode(id)}&token={WebUtility.UrlEncode(token)}").AbsoluteUri;
var customization = Customizations.Value;
string body = TemplateService.Default(
Navigation.BaseUri,
!string.IsNullOrWhiteSpace(customization.LogoLink) ?
customization.LogoLink :
Navigation.ToAbsoluteUri("/img/logo.png").AbsoluteUri,
Localizer["ConfirmEmailTitle"],
string.Format(Localizer["ConfirmEmailBody"], customization.AppName) +
$"""<p style="text-align: center"><a href="{confirmLink}">{Localizer["Submit"]}</a></p>""");
await EmailSender.SendEmailAsync(subscriber.Email, Localizer["ConfirmEmailSubject"], body);
var customization = Customizations.Value;
string body = TemplateService.Default(
Navigation.BaseUri,
!string.IsNullOrWhiteSpace(customization.LogoLink) ?
customization.LogoLink :
Navigation.ToAbsoluteUri("/img/logo.png").AbsoluteUri,
Localizer["ConfirmEmailTitle"],
string.Format(Localizer["ConfirmEmailBody"], customization.AppName) +
$"""<p style="text-align: center"><a href="{confirmLink}">{Localizer["Submit"]}</a></p>""");
await EmailSender.SendEmailAsync(subscriber.Email, Localizer["ConfirmEmailSubject"], body);
}
} catch (Exception ex) {
Logger.LogError(ex, "Failed to create subscriber/send confirmation mail.");
Message = Localizer["Failure_Message"];
}
}

View file

@ -1,18 +1,46 @@
using System.Text.RegularExpressions;
using Microsoft.Extensions.Caching.Distributed;
using Mjml.Net;
namespace Wave.Services;
public partial class EmailTemplateService(ILogger<EmailTemplateService> logger) {
public partial class EmailTemplateService(ILogger<EmailTemplateService> logger, IDistributedCache tokenCache) {
public enum Constants {
BrowserLink, HomeLink, ContentLogo, ContentTitle, ContentBody, EmailUnsubscribeLink
}
private ILogger<EmailTemplateService> Logger { get; } = logger;
private IMjmlRenderer Renderer { get; } = new MjmlRenderer();
private IDistributedCache TokenCache { get; } = tokenCache;
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) {
return Process("default", new Dictionary<Constants, object?> {
{Constants.HomeLink, url},