Sube capa Infrastructure, Solution y archivos de configuracion del Backend

This commit is contained in:
FrancoRu
2026-09-18 13:36:47 -03:00
parent 2eb4eeef03
commit 1ef9a0bd03
205 changed files with 77651 additions and 0 deletions
@@ -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>