mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-07-16 17:47:22 +00:00
code style cleanup
This commit is contained in:
@@ -13,6 +13,5 @@
|
||||
</tr>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public string? FooterText { get; set; }
|
||||
[Parameter] public string? FooterText { get; set; }
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
<td style="padding: 0; background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); border-radius: 8px 8px 0 0;">
|
||||
<!--[if mso]>
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="background-color: #f59e0b;">
|
||||
<tr>
|
||||
<td style="padding: 30px 40px;">
|
||||
<tr>
|
||||
<td style="padding: 30px 40px;">
|
||||
<![endif]-->
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||
<tr>
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<SupportedPlatform Include="browser" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<SupportedPlatform Include="browser"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference
|
||||
Include="Microsoft.AspNetCore.Components.Web"
|
||||
Version="10.0.1"
|
||||
/>
|
||||
<PackageReference
|
||||
Include="Microsoft.Extensions.DependencyInjection.Abstractions"
|
||||
Version="10.0.1"
|
||||
/>
|
||||
<PackageReference
|
||||
Include="Microsoft.Extensions.Logging.Abstractions"
|
||||
Version="10.0.1"
|
||||
/>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference
|
||||
Include="Microsoft.AspNetCore.Components.Web"
|
||||
Version="10.0.1"
|
||||
/>
|
||||
<PackageReference
|
||||
Include="Microsoft.Extensions.DependencyInjection.Abstractions"
|
||||
Version="10.0.1"
|
||||
/>
|
||||
<PackageReference
|
||||
Include="Microsoft.Extensions.Logging.Abstractions"
|
||||
Version="10.0.1"
|
||||
/>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
@using Infrastructure.Email.Templates.Components
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
|
||||
xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
@@ -11,107 +10,113 @@
|
||||
<meta name="x-apple-disable-message-reformatting">
|
||||
<title>Resend Confirmation - The Biergarten App</title>
|
||||
<!--[if mso]>
|
||||
<style>
|
||||
* { font-family: Arial, sans-serif !important; }
|
||||
table { border-collapse: collapse; }
|
||||
</style>
|
||||
<![endif]-->
|
||||
<style>
|
||||
* {
|
||||
font-family: Arial, sans-serif !important;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
</style>
|
||||
<![endif]-->
|
||||
<!--[if !mso]><!-->
|
||||
<style>
|
||||
* {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
* {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
</style>
|
||||
<!--<![endif]-->
|
||||
</head>
|
||||
|
||||
<body style="margin:0; padding:0; background-color:#f4f4f4; width:100%;">
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="background-color:#f4f4f4;">
|
||||
<tr>
|
||||
<td align="center" style="padding:40px 10px;">
|
||||
<!--[if mso]>
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="600" style="width:600px;">
|
||||
<tr><td>
|
||||
<![endif]-->
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%"
|
||||
style="max-width:600px; background:#ffffff; border-radius:8px; box-shadow:0 2px 8px rgba(0,0,0,.08);">
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="background-color:#f4f4f4;">
|
||||
<tr>
|
||||
<td align="center" style="padding:40px 10px;">
|
||||
<!--[if mso]>
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="600" style="width:600px;">
|
||||
<tr>
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%"
|
||||
style="max-width:600px; background:#ffffff; border-radius:8px; box-shadow:0 2px 8px rgba(0,0,0,.08);">
|
||||
|
||||
<Header />
|
||||
<Header/>
|
||||
|
||||
<tr>
|
||||
<td style="padding:40px 40px 16px 40px; text-align:center;">
|
||||
<h1 style="margin:0; color:#333333; font-size:26px; font-weight:700;">
|
||||
New Confirmation Link
|
||||
</h1>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:40px 40px 16px 40px; text-align:center;">
|
||||
<h1 style="margin:0; color:#333333; font-size:26px; font-weight:700;">
|
||||
New Confirmation Link
|
||||
</h1>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td style="padding:0 40px 20px 40px; text-align:center;">
|
||||
<p style="margin:0; color:#666666; font-size:16px; line-height:24px;">
|
||||
Hi <strong style="color:#333333;">@Username</strong>, you requested another email confirmation
|
||||
link.
|
||||
Use the button below to verify your account.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:0 40px 20px 40px; text-align:center;">
|
||||
<p style="margin:0; color:#666666; font-size:16px; line-height:24px;">
|
||||
Hi <strong style="color:#333333;">@Username</strong>, you requested another email confirmation
|
||||
link.
|
||||
Use the button below to verify your account.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td style="padding:8px 40px;">
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<!--[if mso]>
|
||||
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word"
|
||||
href="@ConfirmationLink" style="height:50px;v-text-anchor:middle;width:260px;"
|
||||
arcsize="10%" stroke="f" fillcolor="#f59e0b">
|
||||
<w:anchorlock/>
|
||||
<center style="color:#ffffff;font-family:Arial,sans-serif;font-size:16px;font-weight:700;">
|
||||
Confirm Email Again
|
||||
</center>
|
||||
</v:roundrect>
|
||||
<![endif]-->
|
||||
<!--[if !mso]><!-->
|
||||
<a href="@ConfirmationLink" target="_blank" rel="noopener noreferrer"
|
||||
style="display:inline-block; padding:16px 40px; background:#d97706; color:#ffffff; text-decoration:none; border-radius:6px; font-size:16px; font-weight:700;">
|
||||
<tr>
|
||||
<td style="padding:8px 40px;">
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<!--[if mso]>
|
||||
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml"
|
||||
xmlns:w="urn:schemas-microsoft-com:office:word"
|
||||
href="@ConfirmationLink" style="height:50px;v-text-anchor:middle;width:260px;"
|
||||
arcsize="10%" stroke="f" fillcolor="#f59e0b">
|
||||
<w:anchorlock/>
|
||||
<center
|
||||
style="color:#ffffff;font-family:Arial,sans-serif;font-size:16px;font-weight:700;">
|
||||
Confirm Email Again
|
||||
</a>
|
||||
<!--<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</center>
|
||||
</v:roundrect>
|
||||
<![endif]-->
|
||||
<!--[if !mso]><!-->
|
||||
<a href="@ConfirmationLink" target="_blank" rel="noopener noreferrer"
|
||||
style="display:inline-block; padding:16px 40px; background:#d97706; color:#ffffff; text-decoration:none; border-radius:6px; font-size:16px; font-weight:700;">
|
||||
Confirm Email Again
|
||||
</a>
|
||||
<!--<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td style="padding:20px 40px 8px 40px; text-align:center;">
|
||||
<p style="margin:0; color:#999999; font-size:13px; line-height:20px;">
|
||||
This replacement link expires in 24 hours.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:20px 40px 8px 40px; text-align:center;">
|
||||
<p style="margin:0; color:#999999; font-size:13px; line-height:20px;">
|
||||
This replacement link expires in 24 hours.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td style="padding:0 40px 28px 40px; text-align:center;">
|
||||
<p style="margin:0; color:#999999; font-size:13px; line-height:20px;">
|
||||
If you did not request this, you can safely ignore this email.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:0 40px 28px 40px; text-align:center;">
|
||||
<p style="margin:0; color:#999999; font-size:13px; line-height:20px;">
|
||||
If you did not request this, you can safely ignore this email.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<EmailFooter FooterText="Cheers, The Biergarten App Team" />
|
||||
</table>
|
||||
<!--[if mso]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<EmailFooter FooterText="Cheers, The Biergarten App Team"/>
|
||||
</table>
|
||||
<!--[if mso]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public string Username { get; set; } = string.Empty;
|
||||
[Parameter] public string Username { get; set; } = string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public string ConfirmationLink { get; set; } = string.Empty;
|
||||
[Parameter] public string ConfirmationLink { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
@using Infrastructure.Email.Templates.Components
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
|
||||
xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
@@ -11,108 +10,117 @@
|
||||
<meta name="x-apple-disable-message-reformatting">
|
||||
<title>Welcome to The Biergarten App!</title>
|
||||
<!--[if mso]>
|
||||
<style>
|
||||
* { font-family: Arial, sans-serif !important; }
|
||||
table { border-collapse: collapse; }
|
||||
</style>
|
||||
<![endif]-->
|
||||
<style>
|
||||
* {
|
||||
font-family: Arial, sans-serif !important;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
</style>
|
||||
<![endif]-->
|
||||
<!--[if !mso]><!-->
|
||||
<style>
|
||||
* {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
* {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
</style>
|
||||
<!--<![endif]-->
|
||||
</head>
|
||||
|
||||
<body style="margin: 0; padding: 0; background-color: #f4f4f4; width: 100%;">
|
||||
<!-- Wrapper table for email clients -->
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%"
|
||||
style="background-color: #f4f4f4;">
|
||||
<tr>
|
||||
<td align="center" style="padding: 40px 10px;">
|
||||
<!-- Main container -->
|
||||
<!--[if mso]>
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="600" style="width: 600px;">
|
||||
<tr>
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%"
|
||||
style="max-width: 600px; background-color: #ffffff; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
|
||||
<!-- Wrapper table for email clients -->
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%"
|
||||
style="background-color: #f4f4f4;">
|
||||
<tr>
|
||||
<td align="center" style="padding: 40px 10px;">
|
||||
<!-- Main container -->
|
||||
<!--[if mso]>
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="600" style="width: 600px;">
|
||||
<tr>
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%"
|
||||
style="max-width: 600px; background-color: #ffffff; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
|
||||
|
||||
<!-- Include branded header component -->
|
||||
<Header />
|
||||
<!-- Include branded header component -->
|
||||
<Header/>
|
||||
|
||||
<!-- Welcome message -->
|
||||
<tr>
|
||||
<td style="padding: 40px 40px 20px 40px; text-align: center;">
|
||||
<h1 style="margin: 0; font-size: 28px; color: #333333; font-weight: 600;">
|
||||
Welcome Aboard!
|
||||
</h1>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Welcome message -->
|
||||
<tr>
|
||||
<td style="padding: 40px 40px 20px 40px; text-align: center;">
|
||||
<h1 style="margin: 0; font-size: 28px; color: #333333; font-weight: 600;">
|
||||
Welcome Aboard!
|
||||
</h1>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Body content -->
|
||||
<tr>
|
||||
<td style="padding: 10px 40px 30px 40px; text-align: center;">
|
||||
<p style="margin: 0; font-size: 16px; line-height: 24px; color: #666666;">
|
||||
Hi <strong style="color: #333333;">@Username</strong>, we're excited to have you join our
|
||||
community of beer enthusiasts!
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Body content -->
|
||||
<tr>
|
||||
<td style="padding: 10px 40px 30px 40px; text-align: center;">
|
||||
<p style="margin: 0; font-size: 16px; line-height: 24px; color: #666666;">
|
||||
Hi <strong style="color: #333333;">@Username</strong>, we're excited to have you join our
|
||||
community of beer enthusiasts!
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Confirmation button -->
|
||||
<tr>
|
||||
<td style="padding: 10px 40px;">
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<!--[if mso]>
|
||||
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="@ConfirmationLink" style="height:50px;v-text-anchor:middle;width:250px;" arcsize="10%" stroke="f" fillcolor="#f59e0b">
|
||||
<w:anchorlock/>
|
||||
<center style="color:#ffffff;font-family:Arial,sans-serif;font-size:16px;font-weight:600;">Confirm Your Email</center>
|
||||
</v:roundrect>
|
||||
<![endif]-->
|
||||
<!--[if !mso]><!-->
|
||||
<a href="@ConfirmationLink" target="_blank" rel="noopener noreferrer"
|
||||
style="display: inline-block; padding: 16px 48px; background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); color: #ffffff; text-decoration: none; border-radius: 6px; font-size: 16px; font-weight: 600; min-width: 170px; text-align: center; box-shadow: 0 4px 6px rgba(245, 158, 11, 0.3);">
|
||||
<!-- Confirmation button -->
|
||||
<tr>
|
||||
<td style="padding: 10px 40px;">
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<!--[if mso]>
|
||||
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml"
|
||||
xmlns:w="urn:schemas-microsoft-com:office:word" href="@ConfirmationLink"
|
||||
style="height:50px;v-text-anchor:middle;width:250px;" arcsize="10%" stroke="f"
|
||||
fillcolor="#f59e0b">
|
||||
<w:anchorlock/>
|
||||
<center
|
||||
style="color:#ffffff;font-family:Arial,sans-serif;font-size:16px;font-weight:600;">
|
||||
Confirm Your Email
|
||||
</a>
|
||||
<!--<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</center>
|
||||
</v:roundrect>
|
||||
<![endif]-->
|
||||
<!--[if !mso]><!-->
|
||||
<a href="@ConfirmationLink" target="_blank" rel="noopener noreferrer"
|
||||
style="display: inline-block; padding: 16px 48px; background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); color: #ffffff; text-decoration: none; border-radius: 6px; font-size: 16px; font-weight: 600; min-width: 170px; text-align: center; box-shadow: 0 4px 6px rgba(245, 158, 11, 0.3);">
|
||||
Confirm Your Email
|
||||
</a>
|
||||
<!--<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Link expiry notice -->
|
||||
<tr>
|
||||
<td style="padding: 20px 40px 10px 40px; text-align: center;">
|
||||
<p style="margin: 0; font-size: 13px; line-height: 20px; color: #999999;">
|
||||
This confirmation link expires in 24 hours.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Link expiry notice -->
|
||||
<tr>
|
||||
<td style="padding: 20px 40px 10px 40px; text-align: center;">
|
||||
<p style="margin: 0; font-size: 13px; line-height: 20px; color: #999999;">
|
||||
This confirmation link expires in 24 hours.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Footer info -->
|
||||
<EmailFooter FooterText="Cheers, The Biergarten App Team" />
|
||||
</table>
|
||||
<!--[if mso]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
<!-- Footer info -->
|
||||
<EmailFooter FooterText="Cheers, The Biergarten App Team"/>
|
||||
</table>
|
||||
<!--[if mso]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public string Username { get; set; } = string.Empty;
|
||||
[Parameter] public string Username { get; set; } = string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public string ConfirmationLink { get; set; } = string.Empty;
|
||||
[Parameter] public string ConfirmationLink { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
using Infrastructure.Email.Templates.Mail;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.AspNetCore.Components.Web.HtmlRendering;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Infrastructure.Email.Templates.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// Service for rendering Razor email templates to HTML using HtmlRenderer.
|
||||
/// Service for rendering Razor email templates to HTML using HtmlRenderer.
|
||||
/// </summary>
|
||||
/// <param name="serviceProvider">The service provider used to resolve dependencies for the Razor component rendering pipeline.</param>
|
||||
/// <param name="loggerFactory">The logger factory passed to the <see cref="HtmlRenderer"/> used to render components.</param>
|
||||
/// <param name="serviceProvider">
|
||||
/// The service provider used to resolve dependencies for the Razor component rendering
|
||||
/// pipeline.
|
||||
/// </param>
|
||||
/// <param name="loggerFactory">The logger factory passed to the <see cref="HtmlRenderer" /> used to render components.</param>
|
||||
public class EmailTemplateProvider(
|
||||
IServiceProvider serviceProvider,
|
||||
ILoggerFactory loggerFactory
|
||||
) : IEmailTemplateProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Renders the UserRegisteredEmail template with the specified parameters.
|
||||
/// Renders the UserRegisteredEmail template with the specified parameters.
|
||||
/// </summary>
|
||||
/// <param name="username">The username to include in the email</param>
|
||||
/// <param name="confirmationLink">The email confirmation link</param>
|
||||
@@ -26,17 +30,17 @@ public class EmailTemplateProvider(
|
||||
string confirmationLink
|
||||
)
|
||||
{
|
||||
var parameters = new Dictionary<string, object?>
|
||||
Dictionary<string, object?> parameters = new()
|
||||
{
|
||||
{ nameof(UserRegistration.Username), username },
|
||||
{ nameof(UserRegistration.ConfirmationLink), confirmationLink },
|
||||
{ nameof(UserRegistration.ConfirmationLink), confirmationLink }
|
||||
};
|
||||
|
||||
return await RenderComponentAsync<UserRegistration>(parameters);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renders the ResendConfirmation template with the specified parameters.
|
||||
/// Renders the ResendConfirmation template with the specified parameters.
|
||||
/// </summary>
|
||||
/// <param name="username">The username to include in the email</param>
|
||||
/// <param name="confirmationLink">The new confirmation link</param>
|
||||
@@ -46,19 +50,19 @@ public class EmailTemplateProvider(
|
||||
string confirmationLink
|
||||
)
|
||||
{
|
||||
var parameters = new Dictionary<string, object?>
|
||||
Dictionary<string, object?> parameters = new()
|
||||
{
|
||||
{ nameof(ResendConfirmation.Username), username },
|
||||
{ nameof(ResendConfirmation.ConfirmationLink), confirmationLink },
|
||||
{ nameof(ResendConfirmation.ConfirmationLink), confirmationLink }
|
||||
};
|
||||
|
||||
return await RenderComponentAsync<ResendConfirmation>(parameters);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generic method to render any Razor component to HTML.
|
||||
/// Creates a scoped <see cref="HtmlRenderer"/>, dispatches the render onto its renderer thread,
|
||||
/// and returns the resulting HTML string.
|
||||
/// Generic method to render any Razor component to HTML.
|
||||
/// Creates a scoped <see cref="HtmlRenderer" />, dispatches the render onto its renderer thread,
|
||||
/// and returns the resulting HTML string.
|
||||
/// </summary>
|
||||
/// <typeparam name="TComponent">The type of the Razor component to render.</typeparam>
|
||||
/// <param name="parameters">A dictionary of parameter names and values to pass to the component.</param>
|
||||
@@ -68,15 +72,15 @@ public class EmailTemplateProvider(
|
||||
)
|
||||
where TComponent : IComponent
|
||||
{
|
||||
await using var htmlRenderer = new HtmlRenderer(
|
||||
await using HtmlRenderer htmlRenderer = new(
|
||||
serviceProvider,
|
||||
loggerFactory
|
||||
);
|
||||
|
||||
var html = await htmlRenderer.Dispatcher.InvokeAsync(async () =>
|
||||
string html = await htmlRenderer.Dispatcher.InvokeAsync(async () =>
|
||||
{
|
||||
var parameterView = ParameterView.FromDictionary(parameters);
|
||||
var output = await htmlRenderer.RenderComponentAsync<TComponent>(
|
||||
ParameterView parameterView = ParameterView.FromDictionary(parameters);
|
||||
HtmlRootComponent output = await htmlRenderer.RenderComponentAsync<TComponent>(
|
||||
parameterView
|
||||
);
|
||||
|
||||
@@ -85,4 +89,4 @@ public class EmailTemplateProvider(
|
||||
|
||||
return html;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
namespace Infrastructure.Email.Templates.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// Service for rendering Razor email templates to HTML.
|
||||
/// Service for rendering Razor email templates to HTML.
|
||||
/// </summary>
|
||||
public interface IEmailTemplateProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Renders the UserRegisteredEmail template with the specified parameters.
|
||||
/// Renders the UserRegisteredEmail template with the specified parameters.
|
||||
/// </summary>
|
||||
/// <param name="username">The username to include in the email</param>
|
||||
/// <param name="confirmationLink">The email confirmation link</param>
|
||||
@@ -17,7 +17,7 @@ public interface IEmailTemplateProvider
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Renders the ResendConfirmation template with the specified parameters.
|
||||
/// Renders the ResendConfirmation template with the specified parameters.
|
||||
/// </summary>
|
||||
/// <param name="username">The username to include in the email</param>
|
||||
/// <param name="confirmationLink">The new confirmation link</param>
|
||||
@@ -26,4 +26,4 @@ public interface IEmailTemplateProvider
|
||||
string username,
|
||||
string confirmationLink
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
namespace Infrastructure.Email;
|
||||
|
||||
/// <summary>
|
||||
/// Service for sending emails via SMTP.
|
||||
/// Service for sending emails via SMTP.
|
||||
/// </summary>
|
||||
public interface IEmailProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Sends an email to a single recipient.
|
||||
/// Sends an email to a single recipient.
|
||||
/// </summary>
|
||||
/// <param name="to">Recipient email address</param>
|
||||
/// <param name="subject">Email subject line</param>
|
||||
@@ -15,7 +15,7 @@ public interface IEmailProvider
|
||||
Task SendAsync(string to, string subject, string body, bool isHtml = true);
|
||||
|
||||
/// <summary>
|
||||
/// Sends an email to multiple recipients.
|
||||
/// Sends an email to multiple recipients.
|
||||
/// </summary>
|
||||
/// <param name="to">List of recipient email addresses</param>
|
||||
/// <param name="subject">Email subject line</param>
|
||||
@@ -27,4 +27,4 @@ public interface IEmailProvider
|
||||
string body,
|
||||
bool isHtml = true
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>Infrastructure.Email</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>Infrastructure.Email</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MailKit" Version="4.15.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MailKit" Version="4.15.1"/>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -5,37 +5,54 @@ using MimeKit;
|
||||
namespace Infrastructure.Email;
|
||||
|
||||
/// <summary>
|
||||
/// SMTP email service implementation using MailKit.
|
||||
/// Configured via environment variables.
|
||||
/// SMTP email service implementation using MailKit.
|
||||
/// Configured via environment variables.
|
||||
/// </summary>
|
||||
public class SmtpEmailProvider : IEmailProvider
|
||||
{
|
||||
private readonly string _host;
|
||||
private readonly int _port;
|
||||
private readonly string? _username;
|
||||
private readonly string? _password;
|
||||
private readonly bool _useSsl;
|
||||
private readonly string _fromEmail;
|
||||
private readonly string _fromName;
|
||||
private readonly string _host;
|
||||
private readonly string? _password;
|
||||
private readonly int _port;
|
||||
private readonly string? _username;
|
||||
private readonly bool _useSsl;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="SmtpEmailProvider"/>, reading SMTP configuration
|
||||
/// from environment variables.
|
||||
/// Initializes a new instance of <see cref="SmtpEmailProvider" />, reading SMTP configuration
|
||||
/// from environment variables.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Reads the following environment variables:
|
||||
/// <list type="bullet">
|
||||
/// <item><description><c>SMTP_HOST</c> (required) - the SMTP server hostname.</description></item>
|
||||
/// <item><description><c>SMTP_PORT</c> (optional, default "587") - the SMTP server port, must be a valid integer.</description></item>
|
||||
/// <item><description><c>SMTP_USERNAME</c> (optional) - the username used for authentication.</description></item>
|
||||
/// <item><description><c>SMTP_PASSWORD</c> (optional) - the password used for authentication.</description></item>
|
||||
/// <item><description><c>SMTP_USE_SSL</c> (optional, default "true") - whether to use StartTls when connecting.</description></item>
|
||||
/// <item><description><c>SMTP_FROM_EMAIL</c> (required) - the email address used as the sender.</description></item>
|
||||
/// <item><description><c>SMTP_FROM_NAME</c> (optional, default "The Biergarten") - the display name used as the sender.</description></item>
|
||||
/// </list>
|
||||
/// Reads the following environment variables:
|
||||
/// <list type="bullet">
|
||||
/// <item>
|
||||
/// <description><c>SMTP_HOST</c> (required) - the SMTP server hostname.</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description><c>SMTP_PORT</c> (optional, default "587") - the SMTP server port, must be a valid integer.</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description><c>SMTP_USERNAME</c> (optional) - the username used for authentication.</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description><c>SMTP_PASSWORD</c> (optional) - the password used for authentication.</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description><c>SMTP_USE_SSL</c> (optional, default "true") - whether to use StartTls when connecting.</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description><c>SMTP_FROM_EMAIL</c> (required) - the email address used as the sender.</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// <c>SMTP_FROM_NAME</c> (optional, default "The Biergarten") - the display name used as the
|
||||
/// sender.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Thrown when <c>SMTP_HOST</c> or <c>SMTP_FROM_EMAIL</c> is not set, or when <c>SMTP_PORT</c> is not a valid integer.
|
||||
/// Thrown when <c>SMTP_HOST</c> or <c>SMTP_FROM_EMAIL</c> is not set, or when <c>SMTP_PORT</c> is not a valid integer.
|
||||
/// </exception>
|
||||
public SmtpEmailProvider()
|
||||
{
|
||||
@@ -45,19 +62,17 @@ public class SmtpEmailProvider : IEmailProvider
|
||||
"SMTP_HOST environment variable is not set"
|
||||
);
|
||||
|
||||
var portString =
|
||||
string portString =
|
||||
Environment.GetEnvironmentVariable("SMTP_PORT") ?? "587";
|
||||
if (!int.TryParse(portString, out _port))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"SMTP_PORT '{portString}' is not a valid integer"
|
||||
);
|
||||
}
|
||||
|
||||
_username = Environment.GetEnvironmentVariable("SMTP_USERNAME");
|
||||
_password = Environment.GetEnvironmentVariable("SMTP_PASSWORD");
|
||||
|
||||
var useSslString =
|
||||
string useSslString =
|
||||
Environment.GetEnvironmentVariable("SMTP_USE_SSL") ?? "true";
|
||||
_useSsl = bool.Parse(useSslString);
|
||||
|
||||
@@ -73,7 +88,7 @@ public class SmtpEmailProvider : IEmailProvider
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends an email to a single recipient by delegating to the multi-recipient overload.
|
||||
/// Sends an email to a single recipient by delegating to the multi-recipient overload.
|
||||
/// </summary>
|
||||
/// <param name="to">Recipient email address</param>
|
||||
/// <param name="subject">Email subject line</param>
|
||||
@@ -91,15 +106,18 @@ public class SmtpEmailProvider : IEmailProvider
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends an email to multiple recipients using MailKit's <see cref="SmtpClient"/>.
|
||||
/// Connects using StartTls when SSL is enabled (or no encryption otherwise), authenticates
|
||||
/// if credentials were configured, sends the message, and disconnects.
|
||||
/// Sends an email to multiple recipients using MailKit's <see cref="SmtpClient" />.
|
||||
/// Connects using StartTls when SSL is enabled (or no encryption otherwise), authenticates
|
||||
/// if credentials were configured, sends the message, and disconnects.
|
||||
/// </summary>
|
||||
/// <param name="to">List of recipient email addresses</param>
|
||||
/// <param name="subject">Email subject line</param>
|
||||
/// <param name="body">Email body (HTML or plain text)</param>
|
||||
/// <param name="isHtml">Whether the body is HTML (default: true)</param>
|
||||
/// <exception cref="InvalidOperationException">Thrown when connecting, authenticating, or sending via SMTP fails. The original exception is included as the inner exception.</exception>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Thrown when connecting, authenticating, or sending via SMTP fails. The
|
||||
/// original exception is included as the inner exception.
|
||||
/// </exception>
|
||||
public async Task SendAsync(
|
||||
IEnumerable<string> to,
|
||||
string subject,
|
||||
@@ -107,34 +125,27 @@ public class SmtpEmailProvider : IEmailProvider
|
||||
bool isHtml = true
|
||||
)
|
||||
{
|
||||
var message = new MimeMessage();
|
||||
MimeMessage message = new();
|
||||
message.From.Add(new MailboxAddress(_fromName, _fromEmail));
|
||||
|
||||
foreach (var recipient in to)
|
||||
{
|
||||
message.To.Add(MailboxAddress.Parse(recipient));
|
||||
}
|
||||
foreach (string recipient in to) message.To.Add(MailboxAddress.Parse(recipient));
|
||||
|
||||
message.Subject = subject;
|
||||
|
||||
var bodyBuilder = new BodyBuilder();
|
||||
BodyBuilder bodyBuilder = new();
|
||||
if (isHtml)
|
||||
{
|
||||
bodyBuilder.HtmlBody = body;
|
||||
}
|
||||
else
|
||||
{
|
||||
bodyBuilder.TextBody = body;
|
||||
}
|
||||
|
||||
message.Body = bodyBuilder.ToMessageBody();
|
||||
|
||||
using var client = new SmtpClient();
|
||||
using SmtpClient client = new();
|
||||
|
||||
try
|
||||
{
|
||||
// Determine the SecureSocketOptions based on SSL setting
|
||||
var secureSocketOptions = _useSsl
|
||||
SecureSocketOptions secureSocketOptions = _useSsl
|
||||
? SecureSocketOptions.StartTls
|
||||
: SecureSocketOptions.None;
|
||||
|
||||
@@ -145,9 +156,7 @@ public class SmtpEmailProvider : IEmailProvider
|
||||
!string.IsNullOrEmpty(_username)
|
||||
&& !string.IsNullOrEmpty(_password)
|
||||
)
|
||||
{
|
||||
await client.AuthenticateAsync(_username, _password);
|
||||
}
|
||||
|
||||
await client.SendAsync(message);
|
||||
await client.DisconnectAsync(true);
|
||||
@@ -160,4 +169,4 @@ public class SmtpEmailProvider : IEmailProvider
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,12 @@ using System.Security.Claims;
|
||||
namespace Infrastructure.Jwt;
|
||||
|
||||
/// <summary>
|
||||
/// Service for generating and validating JSON Web Tokens (JWTs) used for authentication.
|
||||
/// Service for generating and validating JSON Web Tokens (JWTs) used for authentication.
|
||||
/// </summary>
|
||||
public interface ITokenInfrastructure
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a signed JWT for the given user.
|
||||
/// Generates a signed JWT for the given user.
|
||||
/// </summary>
|
||||
/// <param name="userId">The unique identifier of the user the token is issued for.</param>
|
||||
/// <param name="username">The username of the user, included as a claim.</param>
|
||||
@@ -23,11 +23,14 @@ public interface ITokenInfrastructure
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Validates a JWT and returns the resulting claims principal.
|
||||
/// Validates a JWT and returns the resulting claims principal.
|
||||
/// </summary>
|
||||
/// <param name="token">The JWT string to validate.</param>
|
||||
/// <param name="secret">The symmetric secret used to verify the token's signature.</param>
|
||||
/// <returns>A <see cref="ClaimsPrincipal"/> representing the validated token's claims.</returns>
|
||||
/// <exception cref="Domain.Exceptions.UnauthorizedException">Thrown when the token is invalid, expired, or fails validation.</exception>
|
||||
/// <returns>A <see cref="ClaimsPrincipal" /> representing the validated token's claims.</returns>
|
||||
/// <exception cref="Domain.Exceptions.UnauthorizedException">
|
||||
/// Thrown when the token is invalid, expired, or fails
|
||||
/// validation.
|
||||
/// </exception>
|
||||
Task<ClaimsPrincipal> ValidateJwtAsync(string token, string secret);
|
||||
}
|
||||
@@ -1,23 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>Infrastructure.Jwt</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>Infrastructure.Jwt</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference
|
||||
Include="Microsoft.IdentityModel.JsonWebTokens"
|
||||
Version="8.2.1"
|
||||
/>
|
||||
<PackageReference
|
||||
Include="System.IdentityModel.Tokens.Jwt"
|
||||
Version="8.2.1"
|
||||
/>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference
|
||||
Include="Microsoft.IdentityModel.JsonWebTokens"
|
||||
Version="8.2.1"
|
||||
/>
|
||||
<PackageReference
|
||||
Include="System.IdentityModel.Tokens.Jwt"
|
||||
Version="8.2.1"
|
||||
/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Domain\Domain.Exceptions\Domain.Exceptions.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Domain\Domain.Exceptions\Domain.Exceptions.csproj"/>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using Domain.Exceptions;
|
||||
using Microsoft.IdentityModel.JsonWebTokens;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using JwtRegisteredClaimNames = System.IdentityModel.Tokens.Jwt.JwtRegisteredClaimNames;
|
||||
using Domain.Exceptions;
|
||||
|
||||
namespace Infrastructure.Jwt;
|
||||
|
||||
/// <summary>
|
||||
/// Generates and validates HMAC-SHA256 signed JWTs using <see cref="JsonWebTokenHandler"/>.
|
||||
/// Generates and validates HMAC-SHA256 signed JWTs using <see cref="JsonWebTokenHandler" />.
|
||||
/// </summary>
|
||||
public class JwtInfrastructure : ITokenInfrastructure
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a signed JWT containing the user's ID, username, issued-at time, expiry time,
|
||||
/// and a unique token identifier (JTI), signed using HMAC-SHA256 with the provided secret.
|
||||
/// Generates a signed JWT containing the user's ID, username, issued-at time, expiry time,
|
||||
/// and a unique token identifier (JTI), signed using HMAC-SHA256 with the provided secret.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Sets the following registered claims: <c>sub</c> (userId), <c>unique_name</c> (username),
|
||||
/// <c>iat</c> (current UTC time), <c>exp</c> (expiry), and <c>jti</c> (a newly generated GUID).
|
||||
/// Sets the following registered claims: <c>sub</c> (userId), <c>unique_name</c> (username),
|
||||
/// <c>iat</c> (current UTC time), <c>exp</c> (expiry), and <c>jti</c> (a newly generated GUID).
|
||||
/// </remarks>
|
||||
/// <param name="userId">The unique identifier of the user the token is issued for.</param>
|
||||
/// <param name="username">The username of the user, included as a claim.</param>
|
||||
@@ -32,31 +32,31 @@ public class JwtInfrastructure : ITokenInfrastructure
|
||||
string secret
|
||||
)
|
||||
{
|
||||
var handler = new JsonWebTokenHandler();
|
||||
var key = Encoding.UTF8.GetBytes(secret);
|
||||
var claims = new List<Claim>
|
||||
JsonWebTokenHandler handler = new();
|
||||
byte[] key = Encoding.UTF8.GetBytes(secret);
|
||||
List<Claim> claims = new()
|
||||
{
|
||||
new(JwtRegisteredClaimNames.Sub, userId.ToString()),
|
||||
new(JwtRegisteredClaimNames.UniqueName, username),
|
||||
new(
|
||||
new Claim(JwtRegisteredClaimNames.Sub, userId.ToString()),
|
||||
new Claim(JwtRegisteredClaimNames.UniqueName, username),
|
||||
new Claim(
|
||||
JwtRegisteredClaimNames.Iat,
|
||||
DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString()
|
||||
),
|
||||
new(
|
||||
new Claim(
|
||||
JwtRegisteredClaimNames.Exp,
|
||||
new DateTimeOffset(expiry).ToUnixTimeSeconds().ToString()
|
||||
),
|
||||
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
|
||||
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
|
||||
};
|
||||
|
||||
var tokenDescriptor = new SecurityTokenDescriptor
|
||||
SecurityTokenDescriptor tokenDescriptor = new()
|
||||
{
|
||||
Subject = new ClaimsIdentity(claims),
|
||||
Expires = expiry,
|
||||
SigningCredentials = new SigningCredentials(
|
||||
new SymmetricSecurityKey(key),
|
||||
SecurityAlgorithms.HmacSha256
|
||||
),
|
||||
)
|
||||
};
|
||||
|
||||
return handler.CreateToken(tokenDescriptor);
|
||||
@@ -64,39 +64,39 @@ public class JwtInfrastructure : ITokenInfrastructure
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Validates a JWT's signature and lifetime (issuer and audience validation are disabled),
|
||||
/// using the provided secret as the HMAC-SHA256 symmetric signing key.
|
||||
/// Validates a JWT's signature and lifetime (issuer and audience validation are disabled),
|
||||
/// using the provided secret as the HMAC-SHA256 symmetric signing key.
|
||||
/// </summary>
|
||||
/// <param name="token">The JWT string to validate.</param>
|
||||
/// <param name="secret">The symmetric secret used to verify the token's signature (encoded as UTF-8 bytes).</param>
|
||||
/// <returns>A <see cref="ClaimsPrincipal"/> wrapping the validated token's claims identity.</returns>
|
||||
/// <returns>A <see cref="ClaimsPrincipal" /> wrapping the validated token's claims identity.</returns>
|
||||
/// <exception cref="UnauthorizedException">
|
||||
/// Thrown when the token is invalid, has no claims identity, is expired, or otherwise fails validation
|
||||
/// (including when validation itself throws, e.g. due to a malformed token or signature mismatch).
|
||||
/// Thrown when the token is invalid, has no claims identity, is expired, or otherwise fails validation
|
||||
/// (including when validation itself throws, e.g. due to a malformed token or signature mismatch).
|
||||
/// </exception>
|
||||
public async Task<ClaimsPrincipal> ValidateJwtAsync(
|
||||
string token,
|
||||
string secret
|
||||
)
|
||||
{
|
||||
var handler = new JsonWebTokenHandler();
|
||||
var keyBytes = Encoding.UTF8.GetBytes(
|
||||
JsonWebTokenHandler handler = new();
|
||||
byte[] keyBytes = Encoding.UTF8.GetBytes(
|
||||
secret
|
||||
);
|
||||
var parameters = new TokenValidationParameters
|
||||
TokenValidationParameters parameters = new()
|
||||
{
|
||||
ValidateIssuer = false,
|
||||
ValidateAudience = false,
|
||||
ValidateLifetime = true,
|
||||
IssuerSigningKey = new SymmetricSecurityKey(keyBytes),
|
||||
IssuerSigningKey = new SymmetricSecurityKey(keyBytes)
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var result = await handler.ValidateTokenAsync(token, parameters);
|
||||
TokenValidationResult? result = await handler.ValidateTokenAsync(token, parameters);
|
||||
if (!result.IsValid || result.ClaimsIdentity == null)
|
||||
throw new UnauthorizedAccessException();
|
||||
|
||||
|
||||
return new ClaimsPrincipal(result.ClaimsIdentity);
|
||||
}
|
||||
catch (Exception e)
|
||||
|
||||
@@ -5,7 +5,7 @@ using Konscious.Security.Cryptography;
|
||||
namespace Infrastructure.PasswordHashing;
|
||||
|
||||
/// <summary>
|
||||
/// Hashes and verifies passwords using the Argon2id algorithm via Konscious.Security.Cryptography.
|
||||
/// Hashes and verifies passwords using the Argon2id algorithm via Konscious.Security.Cryptography.
|
||||
/// </summary>
|
||||
public class Argon2Infrastructure : IPasswordInfrastructure
|
||||
{
|
||||
@@ -15,67 +15,67 @@ public class Argon2Infrastructure : IPasswordInfrastructure
|
||||
private const int ArgonMemoryKb = 65536; // 64MB
|
||||
|
||||
/// <summary>
|
||||
/// Hashes a plaintext password using Argon2id with a newly generated 128-bit cryptographically
|
||||
/// random salt, 4 iterations, 64MB of memory, and a degree of parallelism equal to the number of
|
||||
/// available processors (minimum 1).
|
||||
/// Hashes a plaintext password using Argon2id with a newly generated 128-bit cryptographically
|
||||
/// random salt, 4 iterations, 64MB of memory, and a degree of parallelism equal to the number of
|
||||
/// available processors (minimum 1).
|
||||
/// </summary>
|
||||
/// <param name="password">The plaintext password to hash.</param>
|
||||
/// <returns>
|
||||
/// A string of the form <c>"{base64Salt}:{base64Hash}"</c> containing the salt and the resulting
|
||||
/// 256-bit hash, suitable for storage and later verification.
|
||||
/// A string of the form <c>"{base64Salt}:{base64Hash}"</c> containing the salt and the resulting
|
||||
/// 256-bit hash, suitable for storage and later verification.
|
||||
/// </returns>
|
||||
public string Hash(string password)
|
||||
{
|
||||
var salt = RandomNumberGenerator.GetBytes(SaltSize);
|
||||
var argon2 = new Argon2id(Encoding.UTF8.GetBytes(password))
|
||||
byte[] salt = RandomNumberGenerator.GetBytes(SaltSize);
|
||||
Argon2id argon2 = new(Encoding.UTF8.GetBytes(password))
|
||||
{
|
||||
Salt = salt,
|
||||
DegreeOfParallelism = Math.Max(Environment.ProcessorCount, 1),
|
||||
MemorySize = ArgonMemoryKb,
|
||||
Iterations = ArgonIterations,
|
||||
Iterations = ArgonIterations
|
||||
};
|
||||
|
||||
var hash = argon2.GetBytes(HashSize);
|
||||
byte[] hash = argon2.GetBytes(HashSize);
|
||||
return $"{Convert.ToBase64String(salt)}:{Convert.ToBase64String(hash)}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies a plaintext password against a stored salt/hash string by recomputing the Argon2id
|
||||
/// hash with the extracted salt and comparing it to the stored hash using a fixed-time comparison
|
||||
/// to mitigate timing attacks.
|
||||
/// Verifies a plaintext password against a stored salt/hash string by recomputing the Argon2id
|
||||
/// hash with the extracted salt and comparing it to the stored hash using a fixed-time comparison
|
||||
/// to mitigate timing attacks.
|
||||
/// </summary>
|
||||
/// <param name="password">The plaintext password to verify.</param>
|
||||
/// <param name="stored">
|
||||
/// The stored string of the form <c>"{base64Salt}:{base64Hash}"</c> previously produced by <see cref="Hash"/>.
|
||||
/// The stored string of the form <c>"{base64Salt}:{base64Hash}"</c> previously produced by <see cref="Hash" />.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the password matches the stored hash; <c>false</c> if it does not match, the stored
|
||||
/// string is malformed (e.g. not in the expected two-part format, or not valid base64), or any other
|
||||
/// error occurs while verifying.
|
||||
/// <c>true</c> if the password matches the stored hash; <c>false</c> if it does not match, the stored
|
||||
/// string is malformed (e.g. not in the expected two-part format, or not valid base64), or any other
|
||||
/// error occurs while verifying.
|
||||
/// </returns>
|
||||
public bool Verify(string password, string stored)
|
||||
{
|
||||
try
|
||||
{
|
||||
var parts = stored.Split(
|
||||
string[] parts = stored.Split(
|
||||
':',
|
||||
StringSplitOptions.RemoveEmptyEntries
|
||||
);
|
||||
if (parts.Length != 2)
|
||||
return false;
|
||||
|
||||
var salt = Convert.FromBase64String(parts[0]);
|
||||
var expected = Convert.FromBase64String(parts[1]);
|
||||
byte[] salt = Convert.FromBase64String(parts[0]);
|
||||
byte[] expected = Convert.FromBase64String(parts[1]);
|
||||
|
||||
var argon2 = new Argon2id(Encoding.UTF8.GetBytes(password))
|
||||
Argon2id argon2 = new(Encoding.UTF8.GetBytes(password))
|
||||
{
|
||||
Salt = salt,
|
||||
DegreeOfParallelism = Math.Max(Environment.ProcessorCount, 1),
|
||||
MemorySize = ArgonMemoryKb,
|
||||
Iterations = ArgonIterations,
|
||||
Iterations = ArgonIterations
|
||||
};
|
||||
|
||||
var actual = argon2.GetBytes(expected.Length);
|
||||
byte[] actual = argon2.GetBytes(expected.Length);
|
||||
return CryptographicOperations.FixedTimeEquals(actual, expected);
|
||||
}
|
||||
catch
|
||||
@@ -83,4 +83,4 @@ public class Argon2Infrastructure : IPasswordInfrastructure
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,22 @@
|
||||
namespace Infrastructure.PasswordHashing;
|
||||
|
||||
/// <summary>
|
||||
/// Service for hashing and verifying user passwords.
|
||||
/// Service for hashing and verifying user passwords.
|
||||
/// </summary>
|
||||
public interface IPasswordInfrastructure
|
||||
{
|
||||
/// <summary>
|
||||
/// Hashes a plaintext password, generating a new random salt.
|
||||
/// Hashes a plaintext password, generating a new random salt.
|
||||
/// </summary>
|
||||
/// <param name="password">The plaintext password to hash.</param>
|
||||
/// <returns>A string encoding both the salt and the resulting hash, suitable for storage.</returns>
|
||||
public string Hash(string password);
|
||||
|
||||
/// <summary>
|
||||
/// Verifies a plaintext password against a previously stored hash.
|
||||
/// Verifies a plaintext password against a previously stored hash.
|
||||
/// </summary>
|
||||
/// <param name="password">The plaintext password to verify.</param>
|
||||
/// <param name="stored">The stored salt/hash string previously produced by <see cref="Hash"/>.</param>
|
||||
/// <param name="stored">The stored salt/hash string previously produced by <see cref="Hash" />.</param>
|
||||
/// <returns><c>true</c> if the password matches the stored hash; otherwise <c>false</c>.</returns>
|
||||
public bool Verify(string password, string stored);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>Infrastructure.PasswordHashing</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>Infrastructure.PasswordHashing</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference
|
||||
Include="Konscious.Security.Cryptography.Argon2"
|
||||
Version="1.3.1"
|
||||
/>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference
|
||||
Include="Konscious.Security.Cryptography.Argon2"
|
||||
Version="1.3.1"
|
||||
/>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -5,8 +5,8 @@ using Microsoft.Extensions.Configuration;
|
||||
namespace Infrastructure.Sql;
|
||||
|
||||
/// <summary>
|
||||
/// Default <see cref="ISqlConnectionFactory"/> implementation that creates SQL Server connections,
|
||||
/// resolving the connection string from environment variables or application configuration.
|
||||
/// Default <see cref="ISqlConnectionFactory" /> implementation that creates SQL Server connections,
|
||||
/// resolving the connection string from environment variables or application configuration.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The application configuration, used as a fallback source for the connection string.</param>
|
||||
public class DefaultSqlConnectionFactory(IConfiguration configuration)
|
||||
@@ -17,26 +17,32 @@ public class DefaultSqlConnectionFactory(IConfiguration configuration)
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the SQL Server connection string, preferring (in order): the <c>DB_CONNECTION_STRING</c>
|
||||
/// environment variable, a connection string built from individual <c>DB_*</c> environment variables
|
||||
/// via <see cref="SqlConnectionStringHelper.BuildConnectionString"/>, and finally the <c>"Default"</c>
|
||||
/// connection string from <paramref name="configuration"/>.
|
||||
/// Creates a new, unopened <see cref="SqlConnection" /> using the resolved connection string.
|
||||
/// </summary>
|
||||
/// <returns>A new <see cref="SqlConnection" /> instance.</returns>
|
||||
public DbConnection CreateConnection()
|
||||
{
|
||||
return new SqlConnection(_connectionString);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the SQL Server connection string, preferring (in order): the <c>DB_CONNECTION_STRING</c>
|
||||
/// environment variable, a connection string built from individual <c>DB_*</c> environment variables
|
||||
/// via <see cref="SqlConnectionStringHelper.BuildConnectionString" />, and finally the <c>"Default"</c>
|
||||
/// connection string from <paramref name="configuration" />.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The application configuration to fall back to.</param>
|
||||
/// <returns>The resolved SQL Server connection string.</returns>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Thrown when no connection string can be resolved from any of the supported sources.
|
||||
/// Thrown when no connection string can be resolved from any of the supported sources.
|
||||
/// </exception>
|
||||
private static string GetConnectionString(IConfiguration configuration)
|
||||
{
|
||||
// Check for full connection string first
|
||||
var fullConnectionString = Environment.GetEnvironmentVariable(
|
||||
string? fullConnectionString = Environment.GetEnvironmentVariable(
|
||||
"DB_CONNECTION_STRING"
|
||||
);
|
||||
if (!string.IsNullOrEmpty(fullConnectionString))
|
||||
{
|
||||
return fullConnectionString;
|
||||
}
|
||||
if (!string.IsNullOrEmpty(fullConnectionString)) return fullConnectionString;
|
||||
|
||||
// Try to build from individual environment variables (preferred method for Docker)
|
||||
try
|
||||
@@ -46,24 +52,12 @@ public class DefaultSqlConnectionFactory(IConfiguration configuration)
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// Fall back to configuration-based connection string if env vars are not set
|
||||
var connString = configuration.GetConnectionString("Default");
|
||||
if (!string.IsNullOrEmpty(connString))
|
||||
{
|
||||
return connString;
|
||||
}
|
||||
string? connString = configuration.GetConnectionString("Default");
|
||||
if (!string.IsNullOrEmpty(connString)) return connString;
|
||||
|
||||
throw new InvalidOperationException(
|
||||
"Database connection string not configured. Set DB_CONNECTION_STRING or DB_SERVER, DB_NAME, DB_USER, DB_PASSWORD env vars or ConnectionStrings:Default."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new, unopened <see cref="SqlConnection"/> using the resolved connection string.
|
||||
/// </summary>
|
||||
/// <returns>A new <see cref="SqlConnection"/> instance.</returns>
|
||||
public DbConnection CreateConnection()
|
||||
{
|
||||
return new SqlConnection(_connectionString);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,13 @@ using System.Data.Common;
|
||||
namespace Infrastructure.Sql;
|
||||
|
||||
/// <summary>
|
||||
/// Factory for creating database connections used by repositories.
|
||||
/// Factory for creating database connections used by repositories.
|
||||
/// </summary>
|
||||
public interface ISqlConnectionFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new, unopened database connection.
|
||||
/// Creates a new, unopened database connection.
|
||||
/// </summary>
|
||||
/// <returns>A new <see cref="DbConnection"/> instance.</returns>
|
||||
/// <returns>A new <see cref="DbConnection" /> instance.</returns>
|
||||
DbConnection CreateConnection();
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>Infrastructure.Sql</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.4" />
|
||||
<PackageReference
|
||||
Include="Microsoft.SqlServer.Types"
|
||||
Version="160.1000.6"
|
||||
/>
|
||||
<PackageReference Include="System.Data.SqlClient" Version="4.9.0" />
|
||||
<PackageReference
|
||||
Include="Microsoft.Extensions.Configuration.Abstractions"
|
||||
Version="9.0.0"
|
||||
/>
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>Infrastructure.Sql</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.4"/>
|
||||
<PackageReference
|
||||
Include="Microsoft.SqlServer.Types"
|
||||
Version="160.1000.6"
|
||||
/>
|
||||
<PackageReference Include="System.Data.SqlClient" Version="4.9.0"/>
|
||||
<PackageReference
|
||||
Include="Microsoft.Extensions.Configuration.Abstractions"
|
||||
Version="9.0.0"
|
||||
/>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -3,8 +3,8 @@ using System.Data.Common;
|
||||
namespace Infrastructure.Sql;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for ADO.NET-based repositories, providing shared connection creation and
|
||||
/// entity-mapping infrastructure for derived repository implementations.
|
||||
/// Base class for ADO.NET-based repositories, providing shared connection creation and
|
||||
/// entity-mapping infrastructure for derived repository implementations.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The entity type managed by the repository.</typeparam>
|
||||
/// <param name="connectionFactory">The factory used to create database connections.</param>
|
||||
@@ -12,21 +12,21 @@ public abstract class Repository<T>(ISqlConnectionFactory connectionFactory)
|
||||
where T : class
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates and opens a new database connection using the configured <see cref="ISqlConnectionFactory"/>.
|
||||
/// Creates and opens a new database connection using the configured <see cref="ISqlConnectionFactory" />.
|
||||
/// </summary>
|
||||
/// <returns>An open <see cref="DbConnection"/> ready for use.</returns>
|
||||
/// <returns>An open <see cref="DbConnection" /> ready for use.</returns>
|
||||
/// <exception cref="DbException">Thrown when the connection cannot be opened.</exception>
|
||||
protected async Task<DbConnection> CreateConnection()
|
||||
{
|
||||
var connection = connectionFactory.CreateConnection();
|
||||
DbConnection connection = connectionFactory.CreateConnection();
|
||||
await connection.OpenAsync();
|
||||
return connection;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps the current row of a data reader to an instance of <typeparamref name="T"/>.
|
||||
/// Maps the current row of a data reader to an instance of <typeparamref name="T" />.
|
||||
/// </summary>
|
||||
/// <param name="reader">The data reader positioned on the row to map.</param>
|
||||
/// <returns>The mapped entity instance.</returns>
|
||||
protected abstract T MapToEntity(DbDataReader reader);
|
||||
}
|
||||
}
|
||||
@@ -3,62 +3,62 @@ using Microsoft.Data.SqlClient;
|
||||
namespace Infrastructure.Sql;
|
||||
|
||||
/// <summary>
|
||||
/// Helper for building SQL Server connection strings from environment variables.
|
||||
/// Helper for building SQL Server connection strings from environment variables.
|
||||
/// </summary>
|
||||
public static class SqlConnectionStringHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Builds a SQL Server connection string from environment variables.
|
||||
/// Expects DB_SERVER, DB_NAME, DB_USER, DB_PASSWORD, and DB_TRUST_SERVER_CERTIFICATE.
|
||||
/// Builds a SQL Server connection string from environment variables.
|
||||
/// Expects DB_SERVER, DB_NAME, DB_USER, DB_PASSWORD, and DB_TRUST_SERVER_CERTIFICATE.
|
||||
/// </summary>
|
||||
/// <param name="databaseName">Optional override for the database name. If null, uses DB_NAME env var.</param>
|
||||
/// <returns>A properly formatted SQL Server connection string.</returns>
|
||||
public static string BuildConnectionString(string? databaseName = null)
|
||||
{
|
||||
var server =
|
||||
string server =
|
||||
Environment.GetEnvironmentVariable("DB_SERVER")
|
||||
?? throw new InvalidOperationException(
|
||||
"DB_SERVER environment variable is not set"
|
||||
);
|
||||
|
||||
var dbName =
|
||||
string dbName =
|
||||
databaseName
|
||||
?? Environment.GetEnvironmentVariable("DB_NAME")
|
||||
?? throw new InvalidOperationException(
|
||||
"DB_NAME environment variable is not set"
|
||||
);
|
||||
|
||||
var user =
|
||||
string user =
|
||||
Environment.GetEnvironmentVariable("DB_USER")
|
||||
?? throw new InvalidOperationException(
|
||||
"DB_USER environment variable is not set"
|
||||
);
|
||||
|
||||
var password =
|
||||
string password =
|
||||
Environment.GetEnvironmentVariable("DB_PASSWORD")
|
||||
?? throw new InvalidOperationException(
|
||||
"DB_PASSWORD environment variable is not set"
|
||||
);
|
||||
|
||||
var builder = new SqlConnectionStringBuilder
|
||||
SqlConnectionStringBuilder builder = new()
|
||||
{
|
||||
DataSource = server,
|
||||
InitialCatalog = dbName,
|
||||
UserID = user,
|
||||
Password = password,
|
||||
TrustServerCertificate = true,
|
||||
Encrypt = true,
|
||||
Encrypt = true
|
||||
};
|
||||
|
||||
return builder.ConnectionString;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a connection string to the master database using environment variables.
|
||||
/// Builds a connection string to the master database using environment variables.
|
||||
/// </summary>
|
||||
/// <returns>A connection string for the master database.</returns>
|
||||
public static string BuildMasterConnectionString()
|
||||
{
|
||||
return BuildConnectionString("master");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user