Sube capa Infrastructure, Solution y archivos de configuracion del Backend
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Infrastructure.Email;
|
||||
|
||||
/// <summary>
|
||||
/// Loads HTML email templates from embedded resources and replaces named tokens with their corresponding values.
|
||||
/// </summary>
|
||||
internal static class EmailTemplateLoader
|
||||
{
|
||||
private static readonly Assembly _assembly = typeof(EmailTemplateLoader).Assembly;
|
||||
|
||||
/// <summary>
|
||||
/// Reads the template file and replaces every key in tokens with its corresponding value.
|
||||
/// </summary>
|
||||
/// <param name="templateName">
|
||||
/// File name without extension.
|
||||
/// The file must be at Utils/Helper/Email/Templates/{templateName}.html
|
||||
/// and marked as EmbeddedResource.
|
||||
/// </param>
|
||||
/// <param name="tokens">Dictionary of {{Token}} → replacement value pairs.</param>
|
||||
public static string Render(string templateName, Dictionary<string, string> tokens)
|
||||
{
|
||||
string resourceName =
|
||||
$"Infrastructure.Email.Templates.{templateName}.html";
|
||||
|
||||
using Stream? stream = _assembly.GetManifestResourceStream(resourceName) ?? throw new InvalidOperationException(
|
||||
$"Email template '{templateName}' not found as embedded resource. " +
|
||||
$"Ensure the file exists at 'Email/Templates/{templateName}.html' " +
|
||||
$"and is marked as <EmbeddedResource> in the Infrastructure.csproj.");
|
||||
using StreamReader reader = new(stream);
|
||||
string template = reader.ReadToEnd();
|
||||
|
||||
return tokens.Aggregate(template,
|
||||
(current, token) => current.Replace(token.Key, token.Value));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
using Application.Interfaces.Services;
|
||||
|
||||
using FluentEmail.Core;
|
||||
using FluentEmail.Core.Models;
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Infrastructure.Email;
|
||||
|
||||
/// <summary>
|
||||
/// Transactional email sender backed by FluentEmail and SendGrid.
|
||||
/// </summary>
|
||||
public sealed class FluentEmailHelper(IFluentEmailFactory emailFactory) : IEmailService
|
||||
{
|
||||
public async Task SendPasswordResetAsync(
|
||||
string toEmail, string toUsername, string resetLink,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
string body = EmailTemplateLoader.Render("PasswordResetTemplate", new()
|
||||
{
|
||||
["{{Username}}"] = toUsername,
|
||||
["{{ResetLink}}"] = resetLink,
|
||||
});
|
||||
|
||||
SendResponse result = await emailFactory.Create()
|
||||
.To(toEmail)
|
||||
.Subject("Restablecimiento de contraseña - Club12")
|
||||
.Body(body, isHtml: true)
|
||||
.SendAsync(ct);
|
||||
|
||||
ThrowIfFailed(result, "password reset");
|
||||
}
|
||||
|
||||
public async Task SendWelcomeSetPasswordAsync(
|
||||
string toEmail, string toUsername, string setPasswordLink,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
string body = EmailTemplateLoader.Render("WelcomeSetPasswordTemplate", new()
|
||||
{
|
||||
["{{Username}}"] = toUsername,
|
||||
["{{SetPasswordLink}}"] = setPasswordLink,
|
||||
});
|
||||
|
||||
SendResponse result = await emailFactory.Create()
|
||||
.To(toEmail)
|
||||
.Subject("Activá tu cuenta - Club12")
|
||||
.Body(body, isHtml: true)
|
||||
.SendAsync(ct);
|
||||
|
||||
ThrowIfFailed(result, "welcome set-password");
|
||||
}
|
||||
|
||||
public async Task SendMagicLinkAsync(
|
||||
string toEmail, string toUsername, string magicLink,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
string body = EmailTemplateLoader.Render("MagicLinkTemplate", new()
|
||||
{
|
||||
["{{Username}}"] = toUsername,
|
||||
["{{MagicLink}}"] = magicLink,
|
||||
});
|
||||
|
||||
SendResponse result = await emailFactory.Create()
|
||||
.To(toEmail)
|
||||
.Subject("Tu enlace de acceso - Club12")
|
||||
.Body(body, isHtml: true)
|
||||
.SendAsync(ct);
|
||||
|
||||
ThrowIfFailed(result, "magic link");
|
||||
}
|
||||
|
||||
private static void ThrowIfFailed(SendResponse result, string context)
|
||||
{
|
||||
if (!result.Successful)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Failed to send {context} email: {string.Join(", ", result.ErrorMessages)}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
</head>
|
||||
<body style="margin:0;padding:0;background:#f4f4f5;
|
||||
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;">
|
||||
<table width="100%" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td align="center" style="padding:40px 16px;">
|
||||
<table width="480" cellpadding="0" cellspacing="0"
|
||||
style="background:#fff;border-radius:8px;overflow:hidden;
|
||||
box-shadow:0 1px 3px rgba(0,0,0,.1);">
|
||||
|
||||
<!-- Header -->
|
||||
<tr>
|
||||
<td style="background:#10b981;padding:24px 32px;">
|
||||
<h1 style="margin:0;color:#fff;font-size:20px;font-weight:700;
|
||||
letter-spacing:-.3px;">
|
||||
Club12
|
||||
</h1>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Body -->
|
||||
<tr>
|
||||
<td style="padding:32px;">
|
||||
<h2 style="margin:0 0 16px;color:#111;font-size:18px;font-weight:600;">
|
||||
Tu enlace de acceso
|
||||
</h2>
|
||||
<p style="margin:0 0 12px;color:#444;line-height:1.7;font-size:14px;">
|
||||
Hola, <strong>{{Username}}</strong>.
|
||||
</p>
|
||||
<p style="margin:0 0 12px;color:#444;line-height:1.7;font-size:14px;">
|
||||
Tu enlace de acceso a <strong>Club12</strong> está listo.
|
||||
Hacé clic en el botón para ingresar a tu cuenta.
|
||||
</p>
|
||||
<p style="margin:0 0 28px;color:#e55;font-size:13px;font-weight:600;">
|
||||
⚠ Este enlace expira en 15 minutos y es de un solo uso.
|
||||
</p>
|
||||
<a href="{{MagicLink}}"
|
||||
style="display:inline-block;padding:12px 28px;background:#10b981;
|
||||
color:#fff;border-radius:6px;text-decoration:none;
|
||||
font-weight:600;font-size:14px;">
|
||||
Ingresar a Club12
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Footer -->
|
||||
<tr>
|
||||
<td style="padding:16px 32px;background:#f9fafb;
|
||||
border-top:1px solid #e5e7eb;">
|
||||
<p style="margin:0;color:#9ca3af;font-size:12px;line-height:1.6;">
|
||||
Si no solicitaste este enlace, podés ignorarlo de forma segura.<br />
|
||||
Nadie puede ingresar a tu cuenta sin hacer clic en él.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,68 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
</head>
|
||||
<body style="margin:0;padding:0;background:#f4f4f5;
|
||||
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;">
|
||||
<table width="100%" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td align="center" style="padding:40px 16px;">
|
||||
<table width="480" cellpadding="0" cellspacing="0"
|
||||
style="background:#fff;border-radius:8px;overflow:hidden;
|
||||
box-shadow:0 1px 3px rgba(0,0,0,.1);">
|
||||
|
||||
<!-- Header -->
|
||||
<tr>
|
||||
<td style="background:#0070f3;padding:24px 32px;">
|
||||
<h1 style="margin:0;color:#fff;font-size:20px;font-weight:700;
|
||||
letter-spacing:-.3px;">
|
||||
Club12
|
||||
</h1>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Body -->
|
||||
<tr>
|
||||
<td style="padding:32px;">
|
||||
<h2 style="margin:0 0 16px;color:#111;font-size:18px;font-weight:600;">
|
||||
Restablecimiento de contraseña
|
||||
</h2>
|
||||
<p style="margin:0 0 12px;color:#444;line-height:1.7;font-size:14px;">
|
||||
Hola, <strong>{{Username}}</strong>.
|
||||
</p>
|
||||
<p style="margin:0 0 12px;color:#444;line-height:1.7;font-size:14px;">
|
||||
Un administrador ha solicitado el restablecimiento de tu contraseña
|
||||
en <strong>Club12</strong>. Hacé clic en el botón para establecer
|
||||
una nueva contraseña.
|
||||
</p>
|
||||
<p style="margin:0 0 28px;color:#e55;font-size:13px;font-weight:600;">
|
||||
⚠ Este enlace expira en 24 horas.
|
||||
</p>
|
||||
<a href="{{ResetLink}}"
|
||||
style="display:inline-block;padding:12px 28px;background:#0070f3;
|
||||
color:#fff;border-radius:6px;text-decoration:none;
|
||||
font-weight:600;font-size:14px;">
|
||||
Restablecer contraseña
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Footer -->
|
||||
<tr>
|
||||
<td style="padding:16px 32px;background:#f9fafb;
|
||||
border-top:1px solid #e5e7eb;">
|
||||
<p style="margin:0;color:#9ca3af;font-size:12px;line-height:1.6;">
|
||||
Si no esperabas este correo, podés ignorarlo de forma segura.<br />
|
||||
No se realizarán cambios hasta que uses el enlace.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,56 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
</head>
|
||||
<body style="margin:0;padding:0;background:#f4f4f5;
|
||||
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;">
|
||||
<table width="100%" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td align="center" style="padding:40px 16px;">
|
||||
<table width="480" cellpadding="0" cellspacing="0"
|
||||
style="background:#fff;border-radius:8px;overflow:hidden;
|
||||
box-shadow:0 1px 3px rgba(0,0,0,.1);">
|
||||
<tr>
|
||||
<td style="background:#0070f3;padding:24px 32px;">
|
||||
<h1 style="margin:0;color:#fff;font-size:20px;font-weight:700;">
|
||||
Club12
|
||||
</h1>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td style="padding:32px;">
|
||||
<h2 style="margin:0 0 16px;color:#111;font-size:18px;font-weight:600;">
|
||||
Tu cuenta fue creada
|
||||
</h2>
|
||||
<p style="margin:0 0 12px;color:#444;line-height:1.7;font-size:14px;">
|
||||
Hola, <strong>{{Username}}</strong>.
|
||||
</p>
|
||||
<p style="margin:0 0 12px;color:#444;line-height:1.7;font-size:14px;">
|
||||
Tu cuenta en <strong>Club12</strong> fue creada con una contraseña temporal.
|
||||
Para activar el acceso, definí tu nueva contraseña desde el siguiente enlace.
|
||||
</p>
|
||||
<a href="{{SetPasswordLink}}"
|
||||
style="display:inline-block;padding:12px 28px;background:#0070f3;
|
||||
color:#fff;border-radius:6px;text-decoration:none;
|
||||
font-weight:600;font-size:14px;">
|
||||
Definir contraseña
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td style="padding:16px 32px;background:#f9fafb;border-top:1px solid #e5e7eb;">
|
||||
<p style="margin:0;color:#9ca3af;font-size:12px;line-height:1.6;">
|
||||
Si no esperabas este correo, podés ignorarlo.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user