Skip to content

Commit 13990cb

Browse files
davidortinauCopilot
andcommitted
Wire up ConsoleEmailSender and send confirmation/reset emails
- Register ConsoleEmailSender as IAppEmailSender in both API and WebApp - API Register endpoint now sends confirmation email via IAppEmailSender - API ForgotPassword endpoint now sends reset email via IAppEmailSender - WebApp ForgotPassword endpoint now sends reset email via IAppEmailSender - In dev: emails appear in Aspire structured logs with clickable links - In prod: swap ConsoleEmailSender for SmtpEmailSender Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent a7dacc2 commit 13990cb

File tree

4 files changed

+36
-12
lines changed

4 files changed

+36
-12
lines changed

src/SentenceStudio.Api/Auth/AuthEndpoints.cs

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using Microsoft.AspNetCore.Identity;
22
using Microsoft.EntityFrameworkCore;
33
using SentenceStudio.Data;
4+
using SentenceStudio.Services;
45
using SentenceStudio.Shared.Models;
56

67
namespace SentenceStudio.Api.Auth;
@@ -24,7 +25,9 @@ public static WebApplication MapAuthEndpoints(this WebApplication app)
2425
private static async Task<IResult> Register(
2526
RegisterRequest request,
2627
UserManager<ApplicationUser> userManager,
27-
ApplicationDbContext db)
28+
ApplicationDbContext db,
29+
IAppEmailSender emailSender,
30+
HttpContext httpContext)
2831
{
2932
var user = new ApplicationUser
3033
{
@@ -55,12 +58,15 @@ private static async Task<IResult> Register(
5558
await userManager.UpdateAsync(user);
5659
await db.SaveChangesAsync();
5760

58-
// Generate email confirmation token
61+
// Generate email confirmation token and send confirmation email
5962
var token = await userManager.GenerateEmailConfirmationTokenAsync(user);
60-
// In production, send the confirmation link via IEmailSender.
61-
// For development, the token is generated but email sending is a no-op.
63+
var encodedToken = Uri.EscapeDataString(token);
64+
var baseUrl = $"{httpContext.Request.Scheme}://{httpContext.Request.Host}";
65+
var confirmUrl = $"{baseUrl}/api/auth/confirm-email?userId={user.Id}&token={encodedToken}";
66+
67+
await emailSender.SendConfirmationLinkAsync(user, request.Email, confirmUrl);
6268

63-
return Results.Ok(new { message = "Check your email to confirm your account." });
69+
return Results.Ok(new { message = "Check your email to confirm your account.", userId = user.Id });
6470
}
6571

6672
private static async Task<IResult> Login(
@@ -178,14 +184,19 @@ private static async Task<IResult> ConfirmEmail(
178184

179185
private static async Task<IResult> ForgotPassword(
180186
ForgotPasswordRequest request,
181-
UserManager<ApplicationUser> userManager)
187+
UserManager<ApplicationUser> userManager,
188+
IAppEmailSender emailSender,
189+
HttpContext httpContext)
182190
{
183191
var user = await userManager.FindByEmailAsync(request.Email);
184192
if (user is not null)
185193
{
186194
var token = await userManager.GeneratePasswordResetTokenAsync(user);
187-
// In production, send this token via email.
188-
// Always return 200 to prevent user enumeration.
195+
var encodedToken = Uri.EscapeDataString(token);
196+
var baseUrl = $"{httpContext.Request.Scheme}://{httpContext.Request.Host}";
197+
var resetUrl = $"{baseUrl}/Account/ResetPassword?email={Uri.EscapeDataString(request.Email)}&token={encodedToken}";
198+
199+
await emailSender.SendPasswordResetLinkAsync(user, request.Email, resetUrl);
189200
}
190201

191202
return Results.Ok(new { message = "If that email is registered, a reset link has been sent." });

src/SentenceStudio.Api/Program.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@
9898

9999
builder.Services.AddScoped<JwtTokenService>();
100100

101+
// Email sender — ConsoleEmailSender logs to Aspire structured logs in development;
102+
// swap for SmtpEmailSender in production.
103+
builder.Services.AddSingleton<IAppEmailSender, ConsoleEmailSender>();
104+
101105
builder.Services.AddScoped<ITenantContext, TenantContext>();
102106

103107
// CORS — basic policies for known callers.

src/SentenceStudio.WebApp/Auth/AccountEndpoints.cs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System.Security.Claims;
22
using Microsoft.AspNetCore.Identity;
33
using Microsoft.AspNetCore.Mvc;
4+
using SentenceStudio.Services;
45
using SentenceStudio.Shared.Models;
56

67
namespace SentenceStudio.WebApp.Auth;
@@ -91,15 +92,19 @@ public static void MapAccountEndpoints(this WebApplication app)
9192

9293
group.MapPost("/ForgotPassword", async (
9394
[FromForm] string email,
94-
UserManager<ApplicationUser> userManager) =>
95+
UserManager<ApplicationUser> userManager,
96+
IAppEmailSender emailSender,
97+
HttpContext httpContext) =>
9598
{
9699
var user = await userManager.FindByEmailAsync(email);
97100
if (user is not null)
98101
{
99-
// In production, send the reset token via email.
100-
// For now, just redirect with a confirmation message.
101102
var token = await userManager.GeneratePasswordResetTokenAsync(user);
102-
// TODO: Send email with reset link containing token
103+
var encodedToken = Uri.EscapeDataString(token);
104+
var baseUrl = $"{httpContext.Request.Scheme}://{httpContext.Request.Host}";
105+
var resetUrl = $"{baseUrl}/Account/ResetPassword?email={Uri.EscapeDataString(email)}&token={encodedToken}";
106+
107+
await emailSender.SendPasswordResetLinkAsync(user, email, resetUrl);
103108
}
104109

105110
// Always redirect to avoid email enumeration

src/SentenceStudio.WebApp/Program.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,10 @@
116116

117117
builder.Services.AddAuthorization();
118118

119+
// Email sender — ConsoleEmailSender logs to Aspire structured logs in development;
120+
// swap for SmtpEmailSender in production.
121+
builder.Services.AddSingleton<IAppEmailSender, ConsoleEmailSender>();
122+
119123
builder.Services.AddSingleton<IPreferencesService>(_ => new WebPreferencesService(preferencesPath));
120124
builder.Services.AddSingleton<ISecureStorageService, WebSecureStorageService>();
121125
builder.Services.AddSingleton<IConnectivityService, WebConnectivityService>();

0 commit comments

Comments
 (0)