Sistema de Notificaciones y Baja One-Click
This commit is contained in:
@@ -18,12 +18,21 @@ public class AdminController : ControllerBase
|
||||
private readonly MotoresV2DbContext _context;
|
||||
private readonly IAdSyncService _syncService;
|
||||
private readonly INotificationService _notificationService;
|
||||
private readonly INotificationPreferenceService _prefService;
|
||||
private readonly string _frontendUrl;
|
||||
|
||||
public AdminController(MotoresV2DbContext context, IAdSyncService syncService, INotificationService notificationService)
|
||||
public AdminController(
|
||||
MotoresV2DbContext context,
|
||||
IAdSyncService syncService,
|
||||
INotificationService notificationService,
|
||||
INotificationPreferenceService prefService,
|
||||
IConfiguration config)
|
||||
{
|
||||
_context = context;
|
||||
_syncService = syncService;
|
||||
_notificationService = notificationService;
|
||||
_prefService = prefService;
|
||||
_frontendUrl = config["AppSettings:FrontendUrl"]?.Split(',')[0].Trim() ?? "http://localhost:5173";
|
||||
}
|
||||
|
||||
// --- MODERACIÓN ---
|
||||
@@ -160,12 +169,22 @@ public class AdminController : ControllerBase
|
||||
});
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Sincronizar a Legacy
|
||||
// Sincronizar a Legacy y notificar aprobación (categoría: sistema)
|
||||
try
|
||||
{
|
||||
await _syncService.SyncAdToLegacyAsync(id);
|
||||
var adTitle = $"{ad.Brand?.Name} {ad.VersionName}";
|
||||
await _notificationService.SendAdStatusChangedEmailAsync(ad.User?.Email ?? string.Empty, adTitle, "APROBADO");
|
||||
|
||||
// Generamos el token de baja para la categoría "sistema"
|
||||
string? unsubscribeUrl = null;
|
||||
if (ad.User != null)
|
||||
{
|
||||
var rawToken = await _prefService.GetOrCreateUnsubscribeTokenAsync(ad.User.UserID, NotificationCategory.Sistema);
|
||||
unsubscribeUrl = $"{_frontendUrl}/baja/procesar?token={Uri.EscapeDataString(rawToken)}";
|
||||
}
|
||||
|
||||
await _notificationService.SendAdStatusChangedEmailAsync(
|
||||
ad.User?.Email ?? string.Empty, adTitle, "APROBADO", null, unsubscribeUrl);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
@@ -197,9 +216,19 @@ public class AdminController : ControllerBase
|
||||
});
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Notificar rechazo
|
||||
// Notificar rechazo (categoría: sistema)
|
||||
var adTitle = $"{ad.Brand?.Name} {ad.VersionName}";
|
||||
await _notificationService.SendAdStatusChangedEmailAsync(ad.User?.Email ?? string.Empty, adTitle, "RECHAZADO", reason);
|
||||
|
||||
// Generamos el token de baja para la categoría "sistema"
|
||||
string? unsubscribeUrl = null;
|
||||
if (ad.User != null)
|
||||
{
|
||||
var rawToken = await _prefService.GetOrCreateUnsubscribeTokenAsync(ad.User.UserID, NotificationCategory.Sistema);
|
||||
unsubscribeUrl = $"{_frontendUrl}/baja/procesar?token={Uri.EscapeDataString(rawToken)}";
|
||||
}
|
||||
|
||||
await _notificationService.SendAdStatusChangedEmailAsync(
|
||||
ad.User?.Email ?? string.Empty, adTitle, "RECHAZADO", reason, unsubscribeUrl);
|
||||
|
||||
return Ok(new { message = "Aviso rechazado." });
|
||||
}
|
||||
|
||||
@@ -14,11 +14,19 @@ public class ChatController : ControllerBase
|
||||
{
|
||||
private readonly MotoresV2DbContext _context;
|
||||
private readonly INotificationService _notificationService;
|
||||
private readonly INotificationPreferenceService _prefService;
|
||||
private readonly string _frontendUrl;
|
||||
|
||||
public ChatController(MotoresV2DbContext context, INotificationService notificationService)
|
||||
public ChatController(
|
||||
MotoresV2DbContext context,
|
||||
INotificationService notificationService,
|
||||
INotificationPreferenceService prefService,
|
||||
IConfiguration config)
|
||||
{
|
||||
_context = context;
|
||||
_notificationService = notificationService;
|
||||
_prefService = prefService;
|
||||
_frontendUrl = config["AppSettings:FrontendUrl"]?.Split(',')[0].Trim() ?? "http://localhost:5173";
|
||||
}
|
||||
|
||||
[HttpPost("send")]
|
||||
@@ -39,26 +47,35 @@ public class ChatController : ControllerBase
|
||||
|
||||
if (receiver != null && !string.IsNullOrEmpty(receiver.Email))
|
||||
{
|
||||
// LÓGICA DE NOMBRE DE REMITENTE
|
||||
string senderDisplayName;
|
||||
|
||||
if (sender != null && sender.UserType == 3) // 3 = ADMIN
|
||||
// Solo enviar correo si la preferencia "mensajes" está habilitada
|
||||
if (await _prefService.IsEnabledAsync(receiver.UserID, NotificationCategory.Mensajes))
|
||||
{
|
||||
// Caso: Moderador escribe a Usuario
|
||||
senderDisplayName = "Un moderador de Motores Argentinos";
|
||||
}
|
||||
else
|
||||
{
|
||||
// Caso: Usuario responde a Moderador
|
||||
string name = sender?.UserName ?? "Un usuario";
|
||||
senderDisplayName = $"El usuario {name}";
|
||||
}
|
||||
// LÓGICA DE NOMBRE DE REMITENTE
|
||||
string senderDisplayName;
|
||||
|
||||
await _notificationService.SendChatNotificationEmailAsync(
|
||||
receiver.Email,
|
||||
senderDisplayName, // Pasamos el nombre formateado
|
||||
msg.MessageText,
|
||||
msg.AdID);
|
||||
if (sender != null && sender.UserType == 3) // 3 = ADMIN
|
||||
{
|
||||
// Caso: Moderador escribe a Usuario
|
||||
senderDisplayName = "Un moderador de Motores Argentinos";
|
||||
}
|
||||
else
|
||||
{
|
||||
// Caso: Usuario responde a Moderador
|
||||
string name = sender?.UserName ?? "Un usuario";
|
||||
senderDisplayName = $"El usuario {name}";
|
||||
}
|
||||
|
||||
// Generamos el token de baja para la categoría "mensajes"
|
||||
var rawToken = await _prefService.GetOrCreateUnsubscribeTokenAsync(receiver.UserID, NotificationCategory.Mensajes);
|
||||
var unsubscribeUrl = $"{_frontendUrl}/baja/procesar?token={Uri.EscapeDataString(rawToken)}";
|
||||
|
||||
await _notificationService.SendChatNotificationEmailAsync(
|
||||
receiver.Email,
|
||||
senderDisplayName, // Pasamos el nombre formateado
|
||||
msg.MessageText,
|
||||
msg.AdID,
|
||||
unsubscribeUrl); // Se incluye URL de baja en el footer
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MotoresArgentinosV2.Core.DTOs;
|
||||
using MotoresArgentinosV2.Core.Entities;
|
||||
using MotoresArgentinosV2.Core.Interfaces;
|
||||
using MotoresArgentinosV2.Infrastructure.Data;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using System.Security.Claims;
|
||||
@@ -14,10 +15,14 @@ namespace MotoresArgentinosV2.API.Controllers;
|
||||
public class ProfileController : ControllerBase
|
||||
{
|
||||
private readonly MotoresV2DbContext _context;
|
||||
private readonly INotificationPreferenceService _notifPrefService;
|
||||
|
||||
public ProfileController(MotoresV2DbContext context)
|
||||
public ProfileController(
|
||||
MotoresV2DbContext context,
|
||||
INotificationPreferenceService notifPrefService)
|
||||
{
|
||||
_context = context;
|
||||
_notifPrefService = notifPrefService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@@ -71,4 +76,43 @@ public class ProfileController : ControllerBase
|
||||
|
||||
return Ok(new { message = "Perfil actualizado con éxito." });
|
||||
}
|
||||
|
||||
// ─── Preferencias de Notificación ────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Obtiene las preferencias de notificación del usuario autenticado.
|
||||
/// GET api/profile/notification-preferences
|
||||
/// </summary>
|
||||
[HttpGet("notification-preferences")]
|
||||
public async Task<IActionResult> GetNotificationPreferences()
|
||||
{
|
||||
var userId = int.Parse(User.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? "0");
|
||||
var prefs = await _notifPrefService.GetPreferencesAsync(userId);
|
||||
return Ok(prefs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Actualiza las preferencias de notificación del usuario autenticado.
|
||||
/// PUT api/profile/notification-preferences
|
||||
/// </summary>
|
||||
[HttpPut("notification-preferences")]
|
||||
public async Task<IActionResult> UpdateNotificationPreferences(
|
||||
[FromBody] UpdateNotificationPreferencesDto dto)
|
||||
{
|
||||
var userId = int.Parse(User.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? "0");
|
||||
await _notifPrefService.UpdatePreferencesAsync(userId, dto);
|
||||
|
||||
// Registramos en auditoría
|
||||
_context.AuditLogs.Add(new AuditLog
|
||||
{
|
||||
Action = "NOTIFICATION_PREFS_UPDATED",
|
||||
Entity = "User",
|
||||
EntityID = userId,
|
||||
UserID = userId,
|
||||
Details = "Usuario actualizó sus preferencias de notificación."
|
||||
});
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return Ok(new { message = "Preferencias actualizadas con éxito." });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MotoresArgentinosV2.Core.Interfaces;
|
||||
|
||||
namespace MotoresArgentinosV2.API.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Controlador PÚBLICO (sin autenticación) para gestionar la baja de correos.
|
||||
/// El token del enlace garantiza que no se puede dar de baja a otro usuario.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class UnsubscribeController : ControllerBase
|
||||
{
|
||||
private readonly INotificationPreferenceService _prefService;
|
||||
private readonly IConfiguration _config;
|
||||
|
||||
public UnsubscribeController(
|
||||
INotificationPreferenceService prefService,
|
||||
IConfiguration config)
|
||||
{
|
||||
_prefService = prefService;
|
||||
_config = config;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Procesa la baja one-click desde el enlace del correo.
|
||||
/// GET api/unsubscribe?token=xxxxx
|
||||
/// Redirige al frontend con el resultado para mostrar una página amigable.
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Unsubscribe([FromQuery] string token)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(token))
|
||||
return BadRequest(new { success = false, message = "Token inválido o faltante." });
|
||||
|
||||
var (success, categoryLabel) = await _prefService.UnsubscribeAsync(token);
|
||||
|
||||
if (success)
|
||||
{
|
||||
return Ok(new { success = true, category = categoryLabel });
|
||||
}
|
||||
|
||||
return BadRequest(new { success = false, message = "El enlace de baja ha expirado o no es válido." });
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,8 @@ builder.Logging.AddConsole();
|
||||
builder.Logging.AddDebug();
|
||||
|
||||
// 🔒 CORS POLICY
|
||||
var frontendUrls = (builder.Configuration["AppSettings:FrontendUrl"] ?? "http://localhost:5173" ?? "https://clasificados.eldia.com").Split(',');
|
||||
var frontendUrlConfig = builder.Configuration["AppSettings:FrontendUrl"] ?? "http://localhost:5173,https://clasificados.eldia.com";
|
||||
var frontendUrls = frontendUrlConfig.Split(',');
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy("AllowSpecificOrigin",
|
||||
@@ -109,9 +110,10 @@ builder.Services.AddScoped<IPasswordService, PasswordService>();
|
||||
builder.Services.AddScoped<IIdentityService, IdentityService>();
|
||||
builder.Services.AddScoped<ILegacyPaymentService, LegacyPaymentService>();
|
||||
builder.Services.AddScoped<IPaymentService, MercadoPagoService>();
|
||||
builder.Services.AddScoped<IAdSyncService, AdSyncService>();
|
||||
builder.Services.AddScoped<INotificationService, NotificationService>();
|
||||
builder.Services.AddScoped<ITokenService, TokenService>();
|
||||
builder.Services.AddScoped<IAdSyncService, AdSyncService>();
|
||||
builder.Services.AddScoped<INotificationService, NotificationService>();
|
||||
builder.Services.AddScoped<INotificationPreferenceService, NotificationPreferenceService>();
|
||||
builder.Services.AddScoped<ITokenService, TokenService>();
|
||||
builder.Services.Configure<MailSettings>(builder.Configuration.GetSection("SmtpSettings"));
|
||||
builder.Services.AddScoped<IEmailService, SmtpEmailService>();
|
||||
builder.Services.AddScoped<IImageStorageService, ImageStorageService>();
|
||||
|
||||
Reference in New Issue
Block a user