mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-07-16 17:47:22 +00:00
Migrate Auth and Emails to vertical slices (Features.Auth, Features.Emails)
Auth and Emails land together since Auth's registration/resend flows need Emails to exist first. Service.Auth's four services (Register, Login, Confirmation, Token) collapse into MediatR commands/queries in Features.Auth; TokenService stays as a slice-internal service since multiple handlers call it. Auth no longer references Emails directly — it sends SendRegistrationEmailCommand/SendResendConfirmationEmailCommand (defined in Shared.Application) which Features.Emails handles, so neither slice has a project reference on the other. Service.Emails' IEmailService becomes Features.Emails' IEmailDispatcher, simplified to take (firstName, email, token) instead of a full UserAccount since that's all it ever used. API.Specs' TestApiFactory/MockEmailService are updated to swap in the relocated interface. Also deletes Infrastructure.Repository now that Breweries, UserManagement, and Auth have all moved their repos out of it, and replaces the two now-dead docker-compose test services (repository.tests, service.auth.tests, both pointing at deleted Dockerfiles) with a single unit.tests service that runs every Features.*.Tests project via Dockerfile.tests.
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
using Features.Emails.Services;
|
||||
using MediatR;
|
||||
using Shared.Application.Emails;
|
||||
|
||||
namespace Features.Emails.Commands.SendRegistrationEmail;
|
||||
|
||||
/// <summary>
|
||||
/// Handles <see cref="SendRegistrationEmailCommand"/>, the cross-slice command sent by Features.Auth
|
||||
/// after a new user registers.
|
||||
/// </summary>
|
||||
/// <param name="emailDispatcher">Dispatcher used to render and send the email.</param>
|
||||
public class SendRegistrationEmailHandler(IEmailDispatcher emailDispatcher)
|
||||
: IRequestHandler<SendRegistrationEmailCommand>
|
||||
{
|
||||
public Task Handle(SendRegistrationEmailCommand request, CancellationToken cancellationToken) =>
|
||||
emailDispatcher.SendRegistrationEmailAsync(request.FirstName, request.Email, request.ConfirmationToken);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using Features.Emails.Services;
|
||||
using MediatR;
|
||||
using Shared.Application.Emails;
|
||||
|
||||
namespace Features.Emails.Commands.SendResendConfirmationEmail;
|
||||
|
||||
/// <summary>
|
||||
/// Handles <see cref="SendResendConfirmationEmailCommand"/>, the cross-slice command sent by Features.Auth
|
||||
/// when a user requests a fresh confirmation link.
|
||||
/// </summary>
|
||||
/// <param name="emailDispatcher">Dispatcher used to render and send the email.</param>
|
||||
public class SendResendConfirmationEmailHandler(IEmailDispatcher emailDispatcher)
|
||||
: IRequestHandler<SendResendConfirmationEmailCommand>
|
||||
{
|
||||
public Task Handle(SendResendConfirmationEmailCommand request, CancellationToken cancellationToken) =>
|
||||
emailDispatcher.SendResendConfirmationEmailAsync(request.FirstName, request.Email, request.ConfirmationToken);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using Features.Emails.Services;
|
||||
using Infrastructure.Email;
|
||||
using Infrastructure.Email.Templates.Rendering;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Features.Emails.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// Registers the services owned by the Emails feature slice.
|
||||
/// </summary>
|
||||
public static class FeaturesEmailsServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddFeaturesEmails(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<IEmailProvider, SmtpEmailProvider>();
|
||||
services.AddScoped<IEmailTemplateProvider, EmailTemplateProvider>();
|
||||
services.AddScoped<IEmailDispatcher, EmailDispatcher>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
14
web/backend/Features/Features.Emails/Features.Emails.csproj
Normal file
14
web/backend/Features/Features.Emails/Features.Emails.csproj
Normal file
@@ -0,0 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>Features.Emails</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Email\Infrastructure.Email.csproj" />
|
||||
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Email.Templates\Infrastructure.Email.Templates.csproj" />
|
||||
<ProjectReference Include="..\..\Shared\Shared.Application\Shared.Application.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,73 @@
|
||||
using Infrastructure.Email;
|
||||
using Infrastructure.Email.Templates.Rendering;
|
||||
|
||||
namespace Features.Emails.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation of <see cref="IEmailDispatcher"/> that renders email templates and dispatches
|
||||
/// them via an <see cref="IEmailProvider"/>.
|
||||
/// </summary>
|
||||
/// <param name="emailProvider">Provider used to deliver the rendered emails.</param>
|
||||
/// <param name="emailTemplateProvider">Provider used to render HTML email bodies from templates.</param>
|
||||
public class EmailDispatcher(
|
||||
IEmailProvider emailProvider,
|
||||
IEmailTemplateProvider emailTemplateProvider
|
||||
) : IEmailDispatcher
|
||||
{
|
||||
/// <summary>
|
||||
/// The base URL of the website, used to build confirmation links. Read from the
|
||||
/// <c>WEBSITE_BASE_URL</c> environment variable.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Thrown at type initialization time when the <c>WEBSITE_BASE_URL</c> environment variable is not set.
|
||||
/// </exception>
|
||||
private static readonly string WebsiteBaseUrl =
|
||||
Environment.GetEnvironmentVariable("WEBSITE_BASE_URL")
|
||||
?? throw new InvalidOperationException("WEBSITE_BASE_URL environment variable is not set");
|
||||
|
||||
/// <summary>
|
||||
/// Builds a confirmation link from the given token, renders the registration welcome email template,
|
||||
/// and sends it to the newly created user.
|
||||
/// </summary>
|
||||
public async Task SendRegistrationEmailAsync(string firstName, string email, string confirmationToken)
|
||||
{
|
||||
var confirmationLink =
|
||||
$"{WebsiteBaseUrl}/users/confirm?token={confirmationToken}";
|
||||
|
||||
var emailHtml =
|
||||
await emailTemplateProvider.RenderUserRegisteredEmailAsync(
|
||||
firstName,
|
||||
confirmationLink
|
||||
);
|
||||
|
||||
await emailProvider.SendAsync(
|
||||
email,
|
||||
"Welcome to The Biergarten App!",
|
||||
emailHtml,
|
||||
isHtml: true
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a confirmation link from the given token, renders the resend-confirmation email template,
|
||||
/// and sends it to the user.
|
||||
/// </summary>
|
||||
public async Task SendResendConfirmationEmailAsync(string firstName, string email, string confirmationToken)
|
||||
{
|
||||
var confirmationLink =
|
||||
$"{WebsiteBaseUrl}/users/confirm?token={confirmationToken}";
|
||||
|
||||
var emailHtml =
|
||||
await emailTemplateProvider.RenderResendConfirmationEmailAsync(
|
||||
firstName,
|
||||
confirmationLink
|
||||
);
|
||||
|
||||
await emailProvider.SendAsync(
|
||||
email,
|
||||
"Confirm Your Email - The Biergarten App",
|
||||
emailHtml,
|
||||
isHtml: true
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
namespace Features.Emails.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Defines operations for sending account-related emails, such as registration and confirmation resend emails.
|
||||
/// </summary>
|
||||
public interface IEmailDispatcher
|
||||
{
|
||||
/// <summary>
|
||||
/// Sends a welcome email containing an account confirmation link to a newly registered user.
|
||||
/// </summary>
|
||||
/// <param name="firstName">The recipient's first name, used to personalize the email.</param>
|
||||
/// <param name="email">The recipient's email address.</param>
|
||||
/// <param name="confirmationToken">The confirmation token to embed in the confirmation link.</param>
|
||||
/// <returns>A task that completes once the email has been sent.</returns>
|
||||
Task SendRegistrationEmailAsync(string firstName, string email, string confirmationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Sends an email containing a fresh account confirmation link to a user who requested a resend.
|
||||
/// </summary>
|
||||
/// <param name="firstName">The recipient's first name, used to personalize the email.</param>
|
||||
/// <param name="email">The recipient's email address.</param>
|
||||
/// <param name="confirmationToken">The confirmation token to embed in the confirmation link.</param>
|
||||
/// <returns>A task that completes once the email has been sent.</returns>
|
||||
Task SendResendConfirmationEmailAsync(string firstName, string email, string confirmationToken);
|
||||
}
|
||||
Reference in New Issue
Block a user