Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mail unsubscribe #582

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using ShareBook.Repository;
using ShareBook.Repository.UoW;
using ShareBook.Service;
using ShareBook.Service.Authorization;
using ShareBook.Service.AwsSqs;
using ShareBook.Service.Lgpd;
using ShareBook.Service.Muambator;
Expand Down Expand Up @@ -36,7 +37,8 @@ public static IServiceCollection RegisterRepositoryServices(
services.AddScoped<IAccessHistoryService, AccessHistoryService>();
services.AddScoped<ILgpdService, LgpdService>();
services.AddScoped<IMeetupService, MeetupService>();
services.AddScoped<IRecaptchaService, RecaptchaService>();
services.AddScoped<IRecaptchaService, RecaptchaService>();
services.AddScoped<ICrypto, Crypto>();

//repositories
services.AddScoped<IBookRepository, BookRepository>();
Expand Down
415 changes: 217 additions & 198 deletions ShareBook/ShareBook.Api/Controllers/AccountController.cs

Large diffs are not rendered by default.

85 changes: 85 additions & 0 deletions ShareBook/ShareBook.Service/Authorization/Crypto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
using Microsoft.Extensions.Configuration;
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;

namespace ShareBook.Service.Authorization;

public class Crypto : ICrypto
{

private readonly IConfiguration _configuration;
public string _secret { get; set; }

public Crypto(IConfiguration configuration)
{
_configuration = configuration;
_secret = _configuration["TokenConfigurations:SecretJwtKey"];
}

public string Encrypt(string input)
{
// Converte a chave secreta para bytes
byte[] key = GetKey(_secret);

using (Aes aes = Aes.Create())
{
aes.Key = key;
aes.GenerateIV(); // Gera um IV aleatório

using (MemoryStream ms = new MemoryStream())
{
// Escreve o IV no início do stream
ms.Write(aes.IV, 0, aes.IV.Length);

using (CryptoStream cs = new CryptoStream(ms, aes.CreateEncryptor(), CryptoStreamMode.Write))
{
byte[] inputBytes = Encoding.UTF8.GetBytes(input);
cs.Write(inputBytes, 0, inputBytes.Length);
cs.FlushFinalBlock();
}

// Retorna a string criptografada (Base64)
return Convert.ToBase64String(ms.ToArray());
}
}
}

public string Decrypt(string input)
{
byte[] key = GetKey(_secret);
byte[] inputBytes = Convert.FromBase64String(input);

using (Aes aes = Aes.Create())
{
aes.Key = key;

using (MemoryStream ms = new MemoryStream(inputBytes))
{
// Lê o IV do início do stream
byte[] iv = new byte[16]; // O tamanho do IV para AES é 16 bytes
ms.Read(iv, 0, iv.Length);
aes.IV = iv;

using (CryptoStream cs = new CryptoStream(ms, aes.CreateDecryptor(), CryptoStreamMode.Read))
{
using (StreamReader sr = new StreamReader(cs))
{
// Retorna o texto descriptografado
return sr.ReadToEnd();
}
}
}
}
}

private byte[] GetKey(string secret)
{
// Garante que a chave terá 32 bytes (256 bits) para AES
using (SHA256 sha256 = SHA256.Create())
{
return sha256.ComputeHash(Encoding.UTF8.GetBytes(secret));
}
}
}
7 changes: 7 additions & 0 deletions ShareBook/ShareBook.Service/Authorization/ICrypto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace ShareBook.Service.Authorization;

public interface ICrypto
{
public string Encrypt(string input);
public string Decrypt(string input);
}
23 changes: 11 additions & 12 deletions ShareBook/ShareBook.Service/Authorization/Permission.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
using System.Collections.Generic;

namespace ShareBook.Service.Authorization
namespace ShareBook.Service.Authorization;

public class Permissions
{
public class Permissions
public enum Permission
{
public enum Permission
{
CreateBook,
UpdateBook,
DeleteBook,
ApproveBook,
DonateBook
}

public static List<Permission> AdminPermissions { get; } = new List<Permission>() { Permission.ApproveBook, Permission.DonateBook };
CreateBook,
UpdateBook,
DeleteBook,
ApproveBook,
DonateBook
}

public static List<Permission> AdminPermissions { get; } = new List<Permission>() { Permission.ApproveBook, Permission.DonateBook };
}
Original file line number Diff line number Diff line change
Expand Up @@ -777,9 +777,11 @@ <h3 style="text-align: center;display: block;margin: 0;padding: 0;color: #444444
<td valign="top" class="mcnTextContent" style="padding-top: 0;padding-right: 18px;padding-bottom: 9px;padding-left: 18px;mso-line-height-rule: exactly;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;word-break: break-word;color: #FFFFFF;font-family: Helvetica;font-size: 12px;line-height: 150%;text-align: center;">

<em>
Copyleft © 2023 Sharebook, All rights open source.
Copyleft © 2025 Sharebook, All rights open source.
</em>
<br>

<hr>
Não deseja mais receber esse tipo de email? Tudo bem, use esse <a href="{SharebookBaseUrl}/api/Account/unsubscribe?unsubToken={UnsubToken}" target="_blank">link para se desisncrever</a>.


</td>
Expand Down
4 changes: 3 additions & 1 deletion ShareBook/ShareBook.Service/User/IUserService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ public interface IUserService : IBaseService<User>
Task<IList<User>> GetBySolicitedBookCategoryAsync(Guid bookCategoryId);
Task<UserStatsDTO> GetStatsAsync(Guid? userId);
Task<Result<User>> InsertAsync(RegisterUserDTO userDto);
Task ParentAprovalAsync(string parentHashCodeAproval);
Task ParentAprovalAsync(string parentHashCodeAproval);
Task<string> GenerateUnsubscriptionToken(string email);
Task Unsubscribe(string unsubToken);
}
}
69 changes: 63 additions & 6 deletions ShareBook/ShareBook.Service/User/UserService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using AutoMapper;
using FluentValidation;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using ShareBook.Domain;
using ShareBook.Domain.Common;
using ShareBook.Domain.DTOs;
Expand All @@ -15,6 +16,7 @@
using ShareBook.Repository;
using ShareBook.Repository.Repository;
using ShareBook.Repository.UoW;
using ShareBook.Service.Authorization;
using ShareBook.Service.Generic;
using ShareBook.Service.Recaptcha;

Expand All @@ -26,8 +28,10 @@ public class UserService : BaseService<User>, IUserService
private readonly IBookRepository _bookRepository;
private readonly IUserEmailService _userEmailService;
private readonly IRecaptchaService _recaptchaService;

private readonly IMapper _mapper;
private readonly IMapper _mapper;
private readonly ICrypto _crypto;
private readonly ILogger<UserService> _logger;
private readonly ApplicationDbContext _context;


#region Public
Expand All @@ -37,13 +41,16 @@ public UserService(IUserRepository userRepository, IBookRepository bookRepositor
IValidator<User> validator,
IMapper mapper,
IUserEmailService userEmailService,
IRecaptchaService recaptchaService) : base(userRepository, unitOfWork, validator)
IRecaptchaService recaptchaService, ICrypto crypto, ILogger<UserService> logger, ApplicationDbContext context) : base(userRepository, unitOfWork, validator)
{
_userRepository = userRepository;
_userEmailService = userEmailService;
_bookRepository = bookRepository;
_mapper = mapper;
_recaptchaService = recaptchaService;
_recaptchaService = recaptchaService;
_crypto = crypto;
_logger = logger;
_context = context;
}

public async Task<Result<User>> AuthenticationByEmailAndPasswordAsync(User user)
Expand Down Expand Up @@ -344,9 +351,59 @@ public async Task ParentAprovalAsync(string parentHashCodeAproval)
await _userRepository.UpdateAsync(user);

await _userEmailService.SendEmailParentAprovedNotifyUserAsync(user);
}
}

public async Task<string> GenerateUnsubscriptionToken(string email)
{
var user = await _repository.Get()
.Where(u => u.Email == email)
.FirstOrDefaultAsync();


if (user == null)
throw new ShareBookException(ShareBookException.Error.NotFound, "Nenhum usuário encontrado.");

DateTime expires = DateTime.Now.AddDays(5);
var unsubToken = user.Id.ToString() + "__" + expires.ToString("yyyy-MM-dd");
unsubToken = _crypto.Encrypt(unsubToken);
return unsubToken;
}

public async Task Unsubscribe(string unsubtoken)
{
try
{
string userId;
DateOnly expires;

var tokenData = _crypto.Decrypt(unsubtoken).Split("__");
if (tokenData.Length != 2) throw new Exception("Erro ao desinscrever. Token inválido: " + unsubtoken);

userId = tokenData[0];
expires = DateOnly.ParseExact(tokenData[1], "yyyy-MM-dd");

var user = await _repository.Get()
.Where(u => u.Id == new Guid(userId))
.FirstOrDefaultAsync();
if (user == null) throw new Exception("Erro ao desinscrever. Nenhum usuário encontrado com id: " + userId);

DateOnly today = DateOnly.FromDateTime(DateTime.Now);
if(expires < today) throw new Exception("Erro ao desinscrever. Token expirado em: " + expires.ToString("yyyy-MM-dd"));


// enfim, estando tudo certo, desisncreve o usuário
user.AllowSendingEmail = false;
_context.SaveChanges();
}
catch (Exception ex)
{
// loga o erro, mas não mostra pro usuário por questão de segurança.
_logger.LogError(ex.Message);
throw new Exception("Ocorreu um erro ao desinscrever. Tente novamente mais tarde.");
}
}



#endregion Private
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,8 @@ private async Task<string> GetEmailTemplateAsync(Guid bookId){
BookSlug = book.Slug,
BookImageSlug = book.ImageSlug,
SharebookBaseUrl = _configuration["ServerSettings:DefaultUrl"],
Name = "{Name}"// o MailSender vai trocar pelo nome do usuário.
Name = "{Name}", // o MailSender vai trocar pelo nome do usuário.
UnsubToken = "{UnsubToken}", // o MailSender vai trocar pelo unsub token.
};

return await _emailTemplate.GenerateHtmlFromTemplateAsync("NewBookNotifyTemplate", vm);
Expand Down
17 changes: 12 additions & 5 deletions ShareBook/Sharebook.Jobs/Jobs/7 - MailSender.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,16 @@ public class MailSender : GenericJob, IJob
private readonly MailSenderLowPriorityQueue _sqsLowPriority;
private readonly IConfiguration _configuration;
private string _lastQueue;
private IList<string> _log;
private IList<string> _log;
private readonly IUserService _userService;

public MailSender(
IJobHistoryRepository jobHistoryRepo,
IEmailService emailService,
MailSenderLowPriorityQueue sqsLowPriority,
MailSenderHighPriorityQueue sqsHighPriority,
IConfiguration configuration) : base(jobHistoryRepo)
IConfiguration configuration,
IUserService userService) : base(jobHistoryRepo)
{

JobName = "MailSender";
Expand All @@ -43,7 +45,8 @@ public MailSender(
_sqsLowPriority = sqsLowPriority;
_sqsHighPriority = sqsHighPriority;
_configuration = configuration;
_log = new List<string>();
_log = new List<string>();
_userService = userService;
}

public override async Task<JobHistory> WorkAsync()
Expand Down Expand Up @@ -96,13 +99,17 @@ private async Task<int> SendEmailAsync(SharebookMessage<MailSenderbody> sqsMessa
}

string firstName = GetFirstName(destination.Name);
var bodyHtml2 = bodyHtml.Replace("{name}", firstName, StringComparison.OrdinalIgnoreCase);
var bodyHtml2 = bodyHtml.Replace("{name}", firstName, StringComparison.OrdinalIgnoreCase);

var unsubToken = await _userService.GenerateUnsubscriptionToken(destination.Email);
bodyHtml2 = bodyHtml.Replace("{unsubToken}", unsubToken, StringComparison.OrdinalIgnoreCase);

await _emailService.SendSmtpAsync(destination.Email, destination.Name, bodyHtml2, subject, copyAdmins);

_log.Add($"Enviei um email com SUCESSO para {destination.Email}.");
}
catch(Exception ex) {
RollbarLocator.RollbarInstance.Error(ex);
RollbarLocator.RollbarInstance.Error(ex); // deveria estar abstraído no uso do illoger
_log.Add($"Ocorreu um ERRO ao enviar email para {destination.Email}. Erro: {ex.Message}");
}

Expand Down
Loading