identity: move email sender to common

This commit is contained in:
Abdullah Atta
2025-04-10 12:19:49 +05:00
parent bbabf51073
commit 11dff4f0cc
18 changed files with 422 additions and 360 deletions

View File

@@ -0,0 +1,23 @@
using System.IO;
using WebMarkupMin.Core;
using WebMarkupMin.Core.Loggers;
namespace Streetwriters.Common.Helpers
{
public static class HtmlHelper
{
public static string ReadMinifiedHtmlFile(string path)
{
var settings = new HtmlMinificationSettings()
{
WhitespaceMinificationMode = WhitespaceMinificationMode.Medium,
};
var cssMinifier = new KristensenCssMinifier();
var jsMinifier = new CrockfordJsMinifier();
var minifier = new HtmlMinifier(settings, cssMinifier, jsMinifier, new NullLogger());
return minifier.Minify(File.ReadAllText(path), false).MinifiedContent;
}
}
}

View File

@@ -0,0 +1,19 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using MimeKit;
using MimeKit.Cryptography;
using Streetwriters.Common.Models;
namespace Streetwriters.Common.Interfaces
{
public interface IEmailSender
{
Task SendEmailAsync(
string email,
EmailTemplate template,
IClient client,
GnuPGContext gpgContext = null,
Dictionary<string, byte[]> attachments = null
);
}
}

View File

@@ -0,0 +1,11 @@
namespace Streetwriters.Common.Models
{
public class EmailTemplate
{
public int? Id { get; set; }
public object Data { get; set; }
public string Subject { get; set; }
public string Html { get; set; }
public string Text { get; set; }
}
}

View File

@@ -0,0 +1,134 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using MailKit.Net.Smtp;
using MimeKit;
using MimeKit.Cryptography;
using Org.BouncyCastle.Bcpg;
using Scriban;
using Streetwriters.Common.Interfaces;
using Streetwriters.Common.Models;
namespace Streetwriters.Common.Services
{
public class EmailSender : IEmailSender, IAsyncDisposable
{
private readonly SmtpClient mailClient = new();
public async Task SendEmailAsync(
string email,
EmailTemplate template,
IClient client,
GnuPGContext gpgContext = null,
Dictionary<string, byte[]> attachments = null
)
{
if (!mailClient.IsConnected)
{
if (int.TryParse(Common.Constants.SMTP_PORT, out int port))
{
await mailClient.ConnectAsync(
Common.Constants.SMTP_HOST,
port,
MailKit.Security.SecureSocketOptions.Auto
);
}
else
{
throw new InvalidDataException("SMTP_PORT is not a valid integer value.");
}
}
if (!mailClient.IsAuthenticated)
await mailClient.AuthenticateAsync(
Common.Constants.SMTP_USERNAME,
Common.Constants.SMTP_PASSWORD
);
var message = new MimeMessage();
var sender = new MailboxAddress(client.SenderName, client.SenderEmail);
message.From.Add(sender);
message.To.Add(new MailboxAddress("", email));
message.Subject = await Template.Parse(template.Subject).RenderAsync(template.Data);
if (!string.IsNullOrEmpty(Common.Constants.SMTP_REPLYTO_EMAIL))
message.ReplyTo.Add(MailboxAddress.Parse(Common.Constants.SMTP_REPLYTO_EMAIL));
message.Body = await GetEmailBodyAsync(
template,
client,
sender,
gpgContext,
attachments
);
await mailClient.SendAsync(message);
}
private static async Task<MimeEntity> GetEmailBodyAsync(
EmailTemplate template,
IClient client,
MailboxAddress sender,
GnuPGContext gpgContext = null,
Dictionary<string, byte[]> attachments = null
)
{
var builder = new BodyBuilder();
try
{
builder.TextBody = await Template.Parse(template.Text).RenderAsync(template.Data);
builder.HtmlBody = await Template.Parse(template.Html).RenderAsync(template.Data);
if (attachments != null)
{
foreach (var attachment in attachments)
{
builder.Attachments.Add(attachment.Key, attachment.Value);
}
}
var key = gpgContext?.GetSigningKey(sender);
if (key != null)
{
using (MemoryStream outputStream = new())
{
using (Stream armoredStream = new ArmoredOutputStream(outputStream))
{
key.PublicKey.Encode(armoredStream);
}
outputStream.Seek(0, SeekOrigin.Begin);
builder.Attachments.Add(
$"{client.Id}_pub.asc",
Encoding.ASCII.GetBytes(
Encoding.ASCII.GetString(outputStream.ToArray())
)
);
}
return await MultipartSigned.CreateAsync(
gpgContext,
sender,
DigestAlgorithm.Sha256,
builder.ToMessageBody()
);
}
else
{
return builder.ToMessageBody();
}
}
catch (Exception ex)
{
await Slogger<EmailSender>.Error("GetEmailBodyAsync", ex.ToString());
return builder.ToMessageBody();
}
}
async ValueTask IAsyncDisposable.DisposeAsync()
{
await mailClient.DisconnectAsync(true);
mailClient.Dispose();
}
}
}

View File

@@ -6,13 +6,16 @@
<ItemGroup>
<PackageReference Include="MailKit" Version="4.9.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Cors" Version="2.2.0" />
<PackageReference Include="Scriban" Version="5.12.1" />
<PackageReference Include="WampSharp.Default" Version="20.1.1" />
<PackageReference Include="WampSharp.AspNetCore.WebSockets.Server" Version="20.1.1" />
<PackageReference Include="WampSharp.NewtonsoftMsgpack" Version="20.1.1" />
<PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" />
<PackageReference Include="AspNetCore.Identity.Mongo" Version="8.3.3" />
<PackageReference Include="WebMarkupMin.NUglify" Version="2.18.1" />
</ItemGroup>

View File

@@ -52,10 +52,8 @@ namespace Streetwriters.Identity.Controllers
{
private IPersistedGrantStore PersistedGrantStore { get; set; }
private ITokenGenerationService TokenGenerationService { get; set; }
private IUserClaimsPrincipalFactory<User> PrincipalFactory { get; set; }
private IdentityServerOptions ISOptions { get; set; }
private IUserAccountService UserAccountService { get; set; }
public AccountController(UserManager<User> _userManager, IEmailSender _emailSender,
public AccountController(UserManager<User> _userManager, ITemplatedEmailSender _emailSender,
SignInManager<User> _signInManager, RoleManager<MongoRole> _roleManager, IPersistedGrantStore store,
ITokenGenerationService tokenGenerationService, IMFAService _mfaService, IUserAccountService userAccountService) : base(_userManager, _emailSender, _signInManager, _roleManager, _mfaService)
{
@@ -341,4 +339,4 @@ namespace Streetwriters.Identity.Controllers
});
}
}
}
}

View File

@@ -36,12 +36,12 @@ namespace Streetwriters.Identity.Controllers
protected UserManager<User> UserManager { get; set; }
protected SignInManager<User> SignInManager { get; set; }
protected RoleManager<MongoRole> RoleManager { get; set; }
protected IEmailSender EmailSender { get; set; }
protected ITemplatedEmailSender EmailSender { get; set; }
protected UrlEncoder UrlEncoder { get; set; }
protected IMFAService MFAService { get; set; }
public IdentityControllerBase(
UserManager<User> _userManager,
IEmailSender _emailSender,
ITemplatedEmailSender _emailSender,
SignInManager<User> _signInManager,
RoleManager<MongoRole> _roleManager,
IMFAService _mfaService

View File

@@ -41,7 +41,7 @@ namespace Streetwriters.Identity.Controllers
[Authorize(LocalApi.PolicyName)]
public class MFAController : IdentityControllerBase
{
public MFAController(UserManager<User> _userManager, IEmailSender _emailSender,
public MFAController(UserManager<User> _userManager, ITemplatedEmailSender _emailSender,
SignInManager<User> _signInManager, RoleManager<MongoRole> _roleManager, IMFAService _mfaService) : base(_userManager, _emailSender, _signInManager, _roleManager, _mfaService) { }
[HttpPost]

View File

@@ -39,7 +39,7 @@ namespace Streetwriters.Identity.Controllers
[Route("signup")]
public class SignupController : IdentityControllerBase
{
public SignupController(UserManager<User> _userManager, IEmailSender _emailSender,
public SignupController(UserManager<User> _userManager, ITemplatedEmailSender _emailSender,
SignInManager<User> _signInManager, RoleManager<MongoRole> _roleManager, IMFAService _mfaService) : base(_userManager, _emailSender, _signInManager, _roleManager, _mfaService)
{ }

View File

@@ -1,33 +1,33 @@
/*
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the Affero GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Affero GNU General Public License for more details.
You should have received a copy of the Affero GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System.Threading.Tasks;
using Streetwriters.Common.Interfaces;
namespace Streetwriters.Identity.Interfaces
{
public interface IEmailSender
{
Task SendConfirmationEmailAsync(string email, string callbackUrl, IClient client);
Task SendChangeEmailConfirmationAsync(string email, string code, IClient client);
Task SendPasswordResetEmailAsync(string email, string callbackUrl, IClient client);
Task Send2FACodeEmailAsync(string email, string code, IClient client);
Task SendFailedLoginAlertAsync(string email, string deviceInfo, IClient client);
}
}
/*
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the Affero GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Affero GNU General Public License for more details.
You should have received a copy of the Affero GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System.Threading.Tasks;
using Streetwriters.Common.Interfaces;
namespace Streetwriters.Identity.Interfaces
{
public interface ITemplatedEmailSender
{
Task SendConfirmationEmailAsync(string email, string callbackUrl, IClient client);
Task SendChangeEmailConfirmationAsync(string email, string code, IClient client);
Task SendPasswordResetEmailAsync(string email, string callbackUrl, IClient client);
Task Send2FACodeEmailAsync(string email, string code, IClient client);
Task SendFailedLoginAlertAsync(string email, string deviceInfo, IClient client);
}
}

View File

@@ -1,33 +0,0 @@
/*
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the Affero GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Affero GNU General Public License for more details.
You should have received a copy of the Affero GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using Streetwriters.Identity.Interfaces;
namespace Streetwriters.Identity.Models
{
public class EmailTemplate : IEmailTemplate
{
public int? Id { get; set; }
public object Data { get; set; }
public long? SendAt { get; set; }
public string Subject { get; set; }
public string Html { get; set; }
public string Text { get; set; }
}
}

View File

@@ -1,276 +0,0 @@
/*
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the Affero GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Affero GNU General Public License for more details.
You should have received a copy of the Affero GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System.Threading.Tasks;
using Streetwriters.Identity.Interfaces;
using SendGrid;
using SendGrid.Helpers.Mail;
using Streetwriters.Common;
using Streetwriters.Common.Interfaces;
using Streetwriters.Identity.Models;
using System;
using System.Net.Http;
using System.Text.Json;
using System.Net.Http.Headers;
using System.Text.Json.Serialization;
using MailKit.Net.Smtp;
using MailKit;
using MimeKit;
using System.IO;
using Scriban;
using WebMarkupMin.Core;
using WebMarkupMin.Core.Loggers;
using MimeKit.Cryptography;
using Org.BouncyCastle.Bcpg.OpenPgp;
using System.Linq;
using System.Threading;
using Org.BouncyCastle.Bcpg;
using System.Text;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
namespace Streetwriters.Identity.Services
{
public class EmailSender : IEmailSender, IAsyncDisposable
{
NNGnuPGContext NNGnuPGContext { get; set; }
SmtpClient mailClient;
public EmailSender(IConfiguration configuration)
{
NNGnuPGContext = new NNGnuPGContext(configuration.GetSection("PgpKeySettings"));
mailClient = new SmtpClient();
}
EmailTemplate Email2FATemplate = new EmailTemplate
{
Html = ReadMinifiedHtmlFile("Templates/Email2FACode.html"),
Text = File.ReadAllText("Templates/Email2FACode.txt"),
Subject = "Your {{app_name}} account 2FA code",
};
EmailTemplate ConfirmEmailTemplate = new EmailTemplate
{
Html = ReadMinifiedHtmlFile("Templates/ConfirmEmail.html"),
Text = File.ReadAllText("Templates/ConfirmEmail.txt"),
Subject = "Confirm your {{app_name}} account",
};
EmailTemplate ConfirmChangeEmailTemplate = new EmailTemplate
{
Html = ReadMinifiedHtmlFile("Templates/EmailChangeConfirmation.html"),
Text = File.ReadAllText("Templates/EmailChangeConfirmation.txt"),
Subject = "Change {{app_name}} account email address",
};
EmailTemplate PasswordResetEmailTemplate = new EmailTemplate
{
Html = ReadMinifiedHtmlFile("Templates/ResetAccountPassword.html"),
Text = File.ReadAllText("Templates/ResetAccountPassword.txt"),
Subject = "Reset {{app_name}} account password",
};
EmailTemplate FailedLoginAlertTemplate = new EmailTemplate
{
Html = ReadMinifiedHtmlFile("Templates/FailedLoginAlert.html"),
Text = File.ReadAllText("Templates/FailedLoginAlert.txt"),
Subject = "Failed login attempt on your {{app_name}} account",
};
public async Task Send2FACodeEmailAsync(string email, string code, IClient client)
{
var template = new EmailTemplate
{
Html = Email2FATemplate.Html,
Text = Email2FATemplate.Text,
Subject = Email2FATemplate.Subject,
Data = new
{
app_name = client.Name,
code = code
}
};
await SendEmailAsync(email, template, client);
}
public async Task SendConfirmationEmailAsync(string email, string callbackUrl, IClient client)
{
var template = new EmailTemplate
{
Html = ConfirmEmailTemplate.Html,
Text = ConfirmEmailTemplate.Text,
Subject = ConfirmEmailTemplate.Subject,
Data = new
{
app_name = client.Name,
confirm_link = callbackUrl
}
};
await SendEmailAsync(email, template, client);
}
public async Task SendChangeEmailConfirmationAsync(string email, string code, IClient client)
{
var template = new EmailTemplate
{
Html = ConfirmChangeEmailTemplate.Html,
Text = ConfirmChangeEmailTemplate.Text,
Subject = ConfirmChangeEmailTemplate.Subject,
Data = new
{
app_name = client.Name,
code = code
}
};
await SendEmailAsync(email, template, client);
}
public async Task SendPasswordResetEmailAsync(string email, string callbackUrl, IClient client)
{
var template = new EmailTemplate
{
Html = PasswordResetEmailTemplate.Html,
Text = PasswordResetEmailTemplate.Text,
Subject = PasswordResetEmailTemplate.Subject,
Data = new
{
app_name = client.Name,
reset_link = callbackUrl
}
};
await SendEmailAsync(email, template, client);
}
public async Task SendFailedLoginAlertAsync(string email, string deviceInfo, IClient client)
{
var template = new EmailTemplate
{
Html = FailedLoginAlertTemplate.Html,
Text = FailedLoginAlertTemplate.Text,
Subject = FailedLoginAlertTemplate.Subject,
Data = new
{
app_name = client.Name,
device_info = deviceInfo.Replace("\n", "<br>")
}
};
await SendEmailAsync(email, template, client);
}
private async Task SendEmailAsync(string email, IEmailTemplate template, IClient client)
{
if (!mailClient.IsConnected)
{
if (int.TryParse(Constants.SMTP_PORT, out int port))
{
await mailClient.ConnectAsync(Constants.SMTP_HOST, port, MailKit.Security.SecureSocketOptions.Auto);
}
else
{
throw new InvalidDataException("SMTP_PORT is not a valid integer value.");
}
}
if (!mailClient.IsAuthenticated)
await mailClient.AuthenticateAsync(Constants.SMTP_USERNAME, Constants.SMTP_PASSWORD);
var message = new MimeMessage();
var sender = new MailboxAddress(client.SenderName, client.SenderEmail);
message.From.Add(sender);
message.To.Add(new MailboxAddress("", email));
message.Subject = await Template.Parse(template.Subject).RenderAsync(template.Data);
if (!string.IsNullOrEmpty(Constants.SMTP_REPLYTO_EMAIL))
message.ReplyTo.Add(MailboxAddress.Parse(Constants.SMTP_REPLYTO_EMAIL));
message.Body = await GetEmailBodyAsync(template, client, sender);
await mailClient.SendAsync(message);
}
private async Task<MimeEntity> GetEmailBodyAsync(IEmailTemplate template, IClient client, MailboxAddress sender)
{
var builder = new BodyBuilder();
try
{
builder.TextBody = await Template.Parse(template.Text).RenderAsync(template.Data);
builder.HtmlBody = await Template.Parse(template.Html).RenderAsync(template.Data);
var key = NNGnuPGContext.GetSigningKey(sender);
if (key != null)
{
using (MemoryStream outputStream = new MemoryStream())
{
using (Stream armoredStream = new ArmoredOutputStream(outputStream))
{
key.PublicKey.Encode(armoredStream);
}
outputStream.Seek(0, SeekOrigin.Begin);
builder.Attachments.Add($"{client.Id}_pub.asc", Encoding.ASCII.GetBytes(Encoding.ASCII.GetString(outputStream.ToArray())));
}
return await MultipartSigned.CreateAsync(NNGnuPGContext, sender, DigestAlgorithm.Sha256, builder.ToMessageBody());
}
else
{
return builder.ToMessageBody();
}
}
catch (Exception ex)
{
await Slogger<EmailSender>.Error("GetEmailBodyAsync", ex.ToString());
return builder.ToMessageBody();
}
}
async ValueTask IAsyncDisposable.DisposeAsync()
{
await mailClient.DisconnectAsync(true);
mailClient.Dispose();
}
static string ReadMinifiedHtmlFile(string path)
{
var settings = new HtmlMinificationSettings()
{
WhitespaceMinificationMode = WhitespaceMinificationMode.Medium
};
var cssMinifier = new KristensenCssMinifier();
var jsMinifier = new CrockfordJsMinifier();
var minifier = new HtmlMinifier(settings, cssMinifier, jsMinifier, new NullLogger());
return minifier.Minify(File.ReadAllText(path), false).MinifiedContent;
}
}
class NNGnuPGContext : GnuPGContext
{
IConfiguration PgpKeySettings { get; set; }
public NNGnuPGContext(IConfiguration pgpKeySettings)
{
PgpKeySettings = pgpKeySettings;
}
protected override string GetPasswordForKey(PgpSecretKey key)
{
return PgpKeySettings[key.KeyId.ToString("X")];
}
}
}

View File

@@ -39,9 +39,9 @@ namespace Streetwriters.Identity.Services
const string SMS_ID_CLAIM = "mfa:sms:id";
private UserManager<User> UserManager { get; set; }
private IEmailSender EmailSender { get; set; }
private ITemplatedEmailSender EmailSender { get; set; }
private ISMSSender SMSSender { get; set; }
public MFAService(UserManager<User> _userManager, IEmailSender emailSender, ISMSSender smsSender)
public MFAService(UserManager<User> _userManager, ITemplatedEmailSender emailSender, ISMSSender smsSender)
{
UserManager = _userManager;
EmailSender = emailSender;

View File

@@ -0,0 +1,184 @@
/*
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the Affero GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Affero GNU General Public License for more details.
You should have received a copy of the Affero GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using MailKit;
using MailKit.Net.Smtp;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using MimeKit;
using MimeKit.Cryptography;
using Org.BouncyCastle.Bcpg;
using Org.BouncyCastle.Bcpg.OpenPgp;
using Scriban;
using SendGrid;
using SendGrid.Helpers.Mail;
using Streetwriters.Common;
using Streetwriters.Common.Helpers;
using Streetwriters.Common.Interfaces;
using Streetwriters.Common.Models;
using Streetwriters.Identity.Interfaces;
using Streetwriters.Identity.Models;
using WebMarkupMin.Core;
using WebMarkupMin.Core.Loggers;
namespace Streetwriters.Identity.Services
{
public class TemplatedEmailSender : ITemplatedEmailSender
{
NNGnuPGContext NNGnuPGContext { get; set; }
IEmailSender EmailSender { get; set; }
public TemplatedEmailSender(IConfiguration configuration, IEmailSender emailSender)
{
NNGnuPGContext = new NNGnuPGContext(configuration.GetSection("PgpKeySettings"));
EmailSender = emailSender;
}
EmailTemplate Email2FATemplate = new EmailTemplate
{
Html = HtmlHelper.ReadMinifiedHtmlFile("Templates/Email2FACode.html"),
Text = File.ReadAllText("Templates/Email2FACode.txt"),
Subject = "Your {{app_name}} account 2FA code",
};
EmailTemplate ConfirmEmailTemplate = new EmailTemplate
{
Html = HtmlHelper.ReadMinifiedHtmlFile("Templates/ConfirmEmail.html"),
Text = File.ReadAllText("Templates/ConfirmEmail.txt"),
Subject = "Confirm your {{app_name}} account",
};
EmailTemplate ConfirmChangeEmailTemplate = new EmailTemplate
{
Html = HtmlHelper.ReadMinifiedHtmlFile("Templates/EmailChangeConfirmation.html"),
Text = File.ReadAllText("Templates/EmailChangeConfirmation.txt"),
Subject = "Change {{app_name}} account email address",
};
EmailTemplate PasswordResetEmailTemplate = new EmailTemplate
{
Html = HtmlHelper.ReadMinifiedHtmlFile("Templates/ResetAccountPassword.html"),
Text = File.ReadAllText("Templates/ResetAccountPassword.txt"),
Subject = "Reset {{app_name}} account password",
};
EmailTemplate FailedLoginAlertTemplate = new EmailTemplate
{
Html = HtmlHelper.ReadMinifiedHtmlFile("Templates/FailedLoginAlert.html"),
Text = File.ReadAllText("Templates/FailedLoginAlert.txt"),
Subject = "Failed login attempt on your {{app_name}} account",
};
public async Task Send2FACodeEmailAsync(string email, string code, IClient client)
{
var template = new EmailTemplate
{
Html = Email2FATemplate.Html,
Text = Email2FATemplate.Text,
Subject = Email2FATemplate.Subject,
Data = new { app_name = client.Name, code = code },
};
await EmailSender.SendEmailAsync(email, template, client, NNGnuPGContext);
}
public async Task SendConfirmationEmailAsync(
string email,
string callbackUrl,
IClient client
)
{
var template = new EmailTemplate
{
Html = ConfirmEmailTemplate.Html,
Text = ConfirmEmailTemplate.Text,
Subject = ConfirmEmailTemplate.Subject,
Data = new { app_name = client.Name, confirm_link = callbackUrl },
};
await EmailSender.SendEmailAsync(email, template, client, NNGnuPGContext);
}
public async Task SendChangeEmailConfirmationAsync(
string email,
string code,
IClient client
)
{
var template = new EmailTemplate
{
Html = ConfirmChangeEmailTemplate.Html,
Text = ConfirmChangeEmailTemplate.Text,
Subject = ConfirmChangeEmailTemplate.Subject,
Data = new { app_name = client.Name, code = code },
};
await EmailSender.SendEmailAsync(email, template, client, NNGnuPGContext);
}
public async Task SendPasswordResetEmailAsync(
string email,
string callbackUrl,
IClient client
)
{
var template = new EmailTemplate
{
Html = PasswordResetEmailTemplate.Html,
Text = PasswordResetEmailTemplate.Text,
Subject = PasswordResetEmailTemplate.Subject,
Data = new { app_name = client.Name, reset_link = callbackUrl },
};
await EmailSender.SendEmailAsync(email, template, client, NNGnuPGContext);
}
public async Task SendFailedLoginAlertAsync(string email, string deviceInfo, IClient client)
{
var template = new EmailTemplate
{
Html = FailedLoginAlertTemplate.Html,
Text = FailedLoginAlertTemplate.Text,
Subject = FailedLoginAlertTemplate.Subject,
Data = new
{
app_name = client.Name,
device_info = deviceInfo.Replace("\n", "<br>"),
},
};
await EmailSender.SendEmailAsync(email, template, client, NNGnuPGContext);
}
}
public class NNGnuPGContext(IConfiguration pgpKeySettings) : GnuPGContext
{
IConfiguration PgpKeySettings { get; set; } = pgpKeySettings;
protected override string GetPasswordForKey(PgpSecretKey key)
{
return PgpKeySettings[key.KeyId.ToString("X")];
}
}
}

View File

@@ -43,6 +43,7 @@ using Streetwriters.Common.Extensions;
using Streetwriters.Common.Interfaces;
using Streetwriters.Common.Messages;
using Streetwriters.Common.Models;
using Streetwriters.Common.Services;
using Streetwriters.Identity.Helpers;
using Streetwriters.Identity.Interfaces;
using Streetwriters.Identity.Jobs;
@@ -69,6 +70,7 @@ namespace Streetwriters.Identity
var connectionString = Constants.MONGODB_CONNECTION_STRING;
services.AddTransient<IEmailSender, EmailSender>();
services.AddTransient<ITemplatedEmailSender, TemplatedEmailSender>();
services.AddTransient<ISMSSender, SMSSender>();
services.AddTransient<IPasswordHasher<User>, Argon2PasswordHasher<User>>();

View File

@@ -8,12 +8,11 @@
<ItemGroup>
<PackageReference Include="DotNetEnv" Version="2.3.0" />
<PackageReference Include="IdentityServer4" Version="4.1.2" />
<PackageReference Include="MailKit" Version="3.4.3" />
<PackageReference Include="MailKit" Version="4.9.0" />
<PackageReference Include="MessageBird" Version="3.2.0" />
<PackageReference Include="Ng.UserAgentService" Version="1.1.2" />
<PackageReference Include="Quartz" Version="3.5.0" />
<PackageReference Include="Quartz.AspNetCore" Version="3.5.0" />
<PackageReference Include="Scriban" Version="5.5.1" />
<PackageReference Include="SendGrid" Version="9.24.4" />
<PackageReference Include="Serilog.AspNetCore" Version="3.4.0" />
<PackageReference Include="Geralt" Version="3.1.0" />
@@ -27,8 +26,6 @@
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="7.0.4" />
<PackageReference Include="AspNetCore.Identity.Mongo" Version="8.3.3" />
<PackageReference Include="Twilio" Version="6.13.0" />
<PackageReference Include="WebMarkupMin.Core" Version="2.13.0" />
<PackageReference Include="WebMarkupMin.NUglify" Version="2.12.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -48,8 +48,8 @@ namespace Streetwriters.Identity.Validation
private IHttpContextAccessor HttpContextAccessor { get; set; }
private ITokenValidator TokenValidator { get; set; }
private ITokenGenerationService TokenGenerationService { get; set; }
private IEmailSender EmailSender { get; set; }
public MFAGrantValidator(UserManager<User> userManager, SignInManager<User> signInManager, IMFAService mfaService, IHttpContextAccessor httpContextAccessor, ITokenValidator tokenValidator, ITokenGenerationService tokenGenerationService, IEmailSender emailSender)
private ITemplatedEmailSender EmailSender { get; set; }
public MFAGrantValidator(UserManager<User> userManager, SignInManager<User> signInManager, IMFAService mfaService, IHttpContextAccessor httpContextAccessor, ITokenValidator tokenValidator, ITokenGenerationService tokenGenerationService, ITemplatedEmailSender emailSender)
{
UserManager = userManager;
SignInManager = signInManager;

View File

@@ -40,9 +40,9 @@ namespace Streetwriters.Identity.Validation
private IMFAService MFAService { get; set; }
private IHttpContextAccessor HttpContextAccessor { get; set; }
private ITokenValidator TokenValidator { get; set; }
private IEmailSender EmailSender { get; set; }
private ITemplatedEmailSender EmailSender { get; set; }
public MFAPasswordGrantValidator(UserManager<User> userManager, SignInManager<User> signInManager, IMFAService mfaService, IHttpContextAccessor httpContextAccessor, ITokenValidator tokenValidator, IEmailSender emailSender)
public MFAPasswordGrantValidator(UserManager<User> userManager, SignInManager<User> signInManager, IMFAService mfaService, IHttpContextAccessor httpContextAccessor, ITokenValidator tokenValidator, ITemplatedEmailSender emailSender)
{
UserManager = userManager;
SignInManager = signInManager;