transactions
This commit is contained in:
175
back/services/engine/Crypto/CryptoService.cs
Normal file
175
back/services/engine/Crypto/CryptoService.cs
Normal file
@@ -0,0 +1,175 @@
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace back.services.engine.Crypto;
|
||||
|
||||
public class CryptoService(IMemoryCache cache) : ICryptoService
|
||||
{
|
||||
private readonly IMemoryCache _cache = cache;
|
||||
private readonly MemoryCacheEntryOptions _CacheOptions = new()
|
||||
{
|
||||
AbsoluteExpiration = DateTimeOffset.Now.AddHours(1),
|
||||
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1),
|
||||
SlidingExpiration = TimeSpan.FromMinutes(30),
|
||||
Priority = CacheItemPriority.High,
|
||||
PostEvictionCallbacks =
|
||||
{
|
||||
new PostEvictionCallbackRegistration
|
||||
{
|
||||
EvictionCallback = (key, value, reason, state) =>
|
||||
{
|
||||
var clientId = key.ToString()?.Replace("_public","").Replace("_private","");
|
||||
if(string.IsNullOrEmpty(clientId)) { return; }
|
||||
// Handle the eviction of the certificate - removing public/private keys from the cache
|
||||
try{ cache.Remove($"{clientId}_public"); } catch{ }
|
||||
try{ cache.Remove($"{clientId}_private"); } catch{ }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public string? Encrypt(string clientId,string plainText)
|
||||
{
|
||||
// get keys from cache
|
||||
if (!_cache.TryGetValue($"{clientId}_private", out string? privateCert) || string.IsNullOrEmpty(privateCert))
|
||||
{
|
||||
throw new InvalidOperationException("Private certificate not found for the client.");
|
||||
}
|
||||
if (!_cache.TryGetValue($"{clientId}_public", out string? publicCert) || string.IsNullOrEmpty(publicCert))
|
||||
{
|
||||
throw new InvalidOperationException("Public certificate not found for the client.");
|
||||
}
|
||||
// import rsa keys and configure RSA for encryption
|
||||
using var rsa = RSA.Create(2048);
|
||||
rsa.ImportSubjectPublicKeyInfo(Convert.FromBase64String(publicCert), out _);
|
||||
rsa.ImportRSAPrivateKey(Convert.FromBase64String(privateCert), out _);
|
||||
// Encrypt the plain text using RSA
|
||||
string? encryptedText = null;
|
||||
try
|
||||
{
|
||||
var plainBytes = System.Text.Encoding.UTF8.GetBytes(plainText);
|
||||
var encryptedBytes = rsa.Encrypt(plainBytes, RSAEncryptionPadding.OaepSHA256);
|
||||
encryptedText = Convert.ToBase64String(encryptedBytes);
|
||||
}
|
||||
catch (CryptographicException ex)
|
||||
{
|
||||
// Handle encryption errors
|
||||
throw new InvalidOperationException("Encryption failed.", ex);
|
||||
}
|
||||
return encryptedText;
|
||||
}
|
||||
|
||||
public string? Decrypt(string clientId, string encryptedText)
|
||||
{
|
||||
// get keys from cache
|
||||
if (!_cache.TryGetValue($"{clientId}_private", out string? privateCert) || string.IsNullOrEmpty(privateCert))
|
||||
{
|
||||
throw new InvalidOperationException("Private certificate not found for the client.");
|
||||
}
|
||||
if (!_cache.TryGetValue($"{clientId}_public", out string? publicCert) || string.IsNullOrEmpty(publicCert))
|
||||
{
|
||||
throw new InvalidOperationException("Private certificate not found for the client.");
|
||||
}
|
||||
// import rsa keys and configure RSA for decryption
|
||||
using var rsa = RSA.Create(2048);
|
||||
rsa.ImportSubjectPublicKeyInfo(Convert.FromBase64String(publicCert), out _);
|
||||
rsa.ImportRSAPrivateKey(Convert.FromBase64String(privateCert), out _);
|
||||
// Decrypt the encrypted text using RSA
|
||||
string? plainText = null;
|
||||
try
|
||||
{
|
||||
var encryptedBytes = Convert.FromBase64String(encryptedText);
|
||||
var decryptedBytes = rsa.Decrypt(encryptedBytes, RSAEncryptionPadding.OaepSHA256);
|
||||
plainText = System.Text.Encoding.UTF8.GetString(decryptedBytes);
|
||||
}
|
||||
catch (CryptographicException ex)
|
||||
{
|
||||
// Handle decryption errors
|
||||
throw new InvalidOperationException("Decryption failed.", ex);
|
||||
}
|
||||
return plainText;
|
||||
}
|
||||
|
||||
public string GetPublicCertificate(string clientId)
|
||||
{
|
||||
if (_cache.TryGetValue($"{clientId}_public", out string? publicCert) && !string.IsNullOrEmpty(publicCert))
|
||||
{
|
||||
return publicCert;
|
||||
}
|
||||
(publicCert, string privateCert) = GenerateCertificate();
|
||||
_cache.Set($"{clientId}_public", publicCert, _CacheOptions);
|
||||
_cache.Set($"{clientId}_private", privateCert, _CacheOptions);
|
||||
return publicCert;
|
||||
}
|
||||
|
||||
public string GetPrivateCertificate(string clientId)
|
||||
{
|
||||
if (_cache.TryGetValue($"{clientId}_private", out string? privateCert) && !string.IsNullOrEmpty(privateCert))
|
||||
{
|
||||
return privateCert;
|
||||
}
|
||||
(string publicCert, privateCert) = GenerateCertificate();
|
||||
_cache.Set($"{clientId}_public", publicCert, _CacheOptions);
|
||||
_cache.Set($"{clientId}_private", privateCert, _CacheOptions);
|
||||
return privateCert;
|
||||
}
|
||||
|
||||
private static (string publicCert, string privateCert) GenerateCertificate()
|
||||
{
|
||||
// Generate a new RSA key pair for the client
|
||||
using var rsa = RSA.Create(2048);
|
||||
var publicKey = rsa.ExportSubjectPublicKeyInfo();
|
||||
var privateKey = rsa.ExportRSAPrivateKey();
|
||||
// Convert to Base64 strings for storage
|
||||
var publicCert = Convert.ToBase64String(publicKey);
|
||||
var privateCert = Convert.ToBase64String(privateKey);
|
||||
return (publicCert, privateCert);
|
||||
}
|
||||
|
||||
public string? Hash(string plainText)
|
||||
{
|
||||
string? hash = null;
|
||||
if (string.IsNullOrEmpty(plainText))
|
||||
{
|
||||
return hash;
|
||||
}
|
||||
var plainBytes = System.Text.Encoding.UTF8.GetBytes(plainText);
|
||||
var hashBytes = SHA256.HashData(plainBytes);
|
||||
hash = Convert.ToBase64String(hashBytes);
|
||||
return hash;
|
||||
}
|
||||
|
||||
public bool VerifyHash(string plainText, string hash)
|
||||
{
|
||||
var plainTextHash = Hash(plainText);
|
||||
if (string.IsNullOrEmpty(plainTextHash) || string.IsNullOrEmpty(hash))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return plainTextHash.Equals(hash, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public string Pepper()
|
||||
{
|
||||
// get pepper from environtment variable
|
||||
var pepper = Environment.GetEnvironmentVariable("PEPPER");
|
||||
if (string.IsNullOrEmpty(pepper))
|
||||
{
|
||||
return "BactilForteFlash20mg";
|
||||
}
|
||||
return pepper;
|
||||
}
|
||||
|
||||
public string Salt()
|
||||
{
|
||||
var saltBytes = new byte[32]; // 256 bits
|
||||
using var rng = RandomNumberGenerator.Create();
|
||||
rng.GetBytes(saltBytes);
|
||||
return Convert.ToBase64String(saltBytes);
|
||||
}
|
||||
|
||||
public string? HashPassword(string plainPassword, string plainSalt)
|
||||
{
|
||||
return Hash($"{plainPassword}{plainSalt}{Pepper()}");
|
||||
}
|
||||
}
|
16
back/services/engine/Crypto/ICryptoService.cs
Normal file
16
back/services/engine/Crypto/ICryptoService.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using DependencyInjector.Lifetimes;
|
||||
|
||||
namespace back.services.engine.Crypto;
|
||||
|
||||
public interface ICryptoService : ISingleton
|
||||
{
|
||||
string? Encrypt(string clientId, string plainText);
|
||||
string? Decrypt(string clientId, string encryptedText);
|
||||
string? Hash(string plainText);
|
||||
string? HashPassword(string? plainPassword, string? plainSalt);
|
||||
bool VerifyHash(string plainText, string hash);
|
||||
string Salt();
|
||||
string Pepper();
|
||||
string GetPublicCertificate(string clientId);
|
||||
string GetPrivateCertificate(string clientId);
|
||||
}
|
8
back/services/engine/ImageResizer/IImageResizer.cs
Normal file
8
back/services/engine/ImageResizer/IImageResizer.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using DependencyInjector.Lifetimes;
|
||||
|
||||
namespace back.services.engine.ImageResizer;
|
||||
|
||||
public interface IImageResizer : ISingleton
|
||||
{
|
||||
Task<Stream> ResizeImage(IFormFile image, int v);
|
||||
}
|
23
back/services/engine/ImageResizer/ImageResizer.cs
Normal file
23
back/services/engine/ImageResizer/ImageResizer.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
|
||||
namespace back.services.engine.ImageResizer;
|
||||
|
||||
public sealed class ImageResizer : IImageResizer
|
||||
{
|
||||
public async Task<Stream> ResizeImage(IFormFile image, int maxRes)
|
||||
{
|
||||
if (image == null || image.Length == 0)
|
||||
{
|
||||
throw new ArgumentException("Invalid image file.");
|
||||
}
|
||||
using var inputStream = image.OpenReadStream();
|
||||
using var outputStream = new MemoryStream();
|
||||
using var img = Image.Load(inputStream);
|
||||
|
||||
img.Mutate(x => x.Resize(new ResizeOptions { Size = new Size(maxRes, 0), Mode = ResizeMode.Max }));
|
||||
await img.SaveAsWebpAsync(outputStream);
|
||||
outputStream.Position = 0;
|
||||
return outputStream;
|
||||
}
|
||||
}
|
@@ -0,0 +1,8 @@
|
||||
using DependencyInjector.Lifetimes;
|
||||
|
||||
namespace back.services.engine.PasswordGenerator;
|
||||
|
||||
public interface IPasswordGenerator : ISingleton
|
||||
{
|
||||
string Generate(int length, bool includeNumbers = true, bool includeMayus = true, bool includeMinus = true, bool includeSpecials = true);
|
||||
}
|
24
back/services/engine/PasswordGenerator/PasswordGenerator.cs
Normal file
24
back/services/engine/PasswordGenerator/PasswordGenerator.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
namespace back.services.engine.PasswordGenerator;
|
||||
|
||||
public class PasswordGenerator : IPasswordGenerator
|
||||
{
|
||||
public string Generate(int length, bool includeNumbers = true, bool includeMayus = true, bool includeMinus = true, bool includeSpecials = true)
|
||||
{
|
||||
const string numbers = "0123456789";
|
||||
const string mayus = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
const string minus = "abcdefghijklmnopqrstuvwxyz";
|
||||
const string specials = "!@#$%^&*()_+[]{}|;:,.<>?";
|
||||
var characters = minus;
|
||||
if (includeNumbers) characters += numbers;
|
||||
if (includeMayus) characters += mayus;
|
||||
if (includeSpecials) characters += specials;
|
||||
var random = new Random((int)DateTimeOffset.UtcNow.Ticks);
|
||||
var password = new char[length];
|
||||
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
password[i] = characters[random.Next(characters.Length)];
|
||||
}
|
||||
return new string(password);
|
||||
}
|
||||
}
|
8
back/services/engine/SystemUser/ISystemUserGenerator.cs
Normal file
8
back/services/engine/SystemUser/ISystemUserGenerator.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using DependencyInjector.Lifetimes;
|
||||
|
||||
namespace back.services.engine.SystemUser;
|
||||
|
||||
public interface ISystemUserGenerator: IScoped
|
||||
{
|
||||
Task GenerateAsync();
|
||||
}
|
47
back/services/engine/SystemUser/SystemUserGenerator.cs
Normal file
47
back/services/engine/SystemUser/SystemUserGenerator.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using back.DataModels;
|
||||
using back.persistance.blob;
|
||||
using back.persistance.data;
|
||||
using back.persistance.data.repositories.Abstracts;
|
||||
using back.services.engine.Crypto;
|
||||
using back.services.engine.PasswordGenerator;
|
||||
using Transactional.Abstractions.Interfaces;
|
||||
|
||||
namespace back.services.engine.SystemUser;
|
||||
|
||||
public class SystemUserGenerator(
|
||||
ITransactionalService<DataContext> transactional,
|
||||
IUserRepository userRepository,
|
||||
IPersonRepository personRepository,
|
||||
ICryptoService cryptoService,
|
||||
IBlobStorageService blobStorageService,
|
||||
IPasswordGenerator passwordGenerator) : ISystemUserGenerator
|
||||
{
|
||||
public async Task GenerateAsync()
|
||||
{
|
||||
var systemKey = new SystemKey() {
|
||||
Password = passwordGenerator.Generate(16),
|
||||
};
|
||||
var systemKeyJson = System.Text.Json.JsonSerializer.Serialize(systemKey);
|
||||
|
||||
using Stream stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(systemKeyJson));
|
||||
|
||||
await blobStorageService.Delete("systemkey.lock");
|
||||
|
||||
await blobStorageService.Save(
|
||||
stream,
|
||||
"systemkey.lock"
|
||||
);
|
||||
|
||||
User.SystemUser.Password = systemKey.Password;
|
||||
User.SystemUser.Salt = cryptoService.Salt();
|
||||
User.SystemUser.Password = cryptoService.HashPassword(User.SystemUser.Password, User.SystemUser.Salt) ?? string.Empty;
|
||||
|
||||
if (!await userRepository.Exists(User.SystemUser.Id!))
|
||||
{
|
||||
await transactional.DoTransaction(async () => {
|
||||
await personRepository.Insert(Person.SystemPerson);
|
||||
await userRepository.Insert(User.SystemUser);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
66
back/services/engine/mailing/EmailService.cs
Normal file
66
back/services/engine/mailing/EmailService.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using back.Options;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Net;
|
||||
using System.Net.Mail;
|
||||
|
||||
namespace back.services.engine.mailing;
|
||||
|
||||
public class EmailService(IOptions<MailServerOptions> options) : IEmailService
|
||||
{
|
||||
public async Task SendEmailAsync(List<string> tos, string from, string subject, string body, Dictionary<string, object>? attachments = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Parallel.ForEachAsync(tos, async (to, cancellationToken) => {
|
||||
await SendEmailAsync(to, from, subject, body, attachments, cancellationToken);
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Log the exception or handle it as needed
|
||||
Console.WriteLine($"Error sending email to multiple recipients: {ex.Message}");
|
||||
}
|
||||
}
|
||||
public async Task SendEmailAsync(string to, string from, string subject, string body, Dictionary<string, object>? attachments = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var message = new MailMessage();
|
||||
message.From = new MailAddress(from);
|
||||
message.To.Add(to);
|
||||
message.Subject = subject;
|
||||
message.Body = body;
|
||||
message.IsBodyHtml = true;
|
||||
message.Priority = MailPriority.Normal;
|
||||
message.DeliveryNotificationOptions = DeliveryNotificationOptions.Never;
|
||||
|
||||
if (attachments != null)
|
||||
{
|
||||
foreach (var attachment in attachments)
|
||||
{
|
||||
if (attachment.Value is FileStream fileStream)
|
||||
{
|
||||
message.Attachments.Add(new Attachment(fileStream, attachment.Key));
|
||||
}
|
||||
if (attachment.Value is string filePath && File.Exists(filePath))
|
||||
{
|
||||
message.Attachments.Add(new Attachment(filePath));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
using var cliente = new SmtpClient(options.Value.SmtpServer, options.Value.Puerto);
|
||||
cliente.UseDefaultCredentials = false;
|
||||
cliente.Credentials = new NetworkCredential(options.Value.Usuario, options.Value.Password);
|
||||
cliente.EnableSsl = options.Value.EnableSsl;
|
||||
cliente.DeliveryMethod = SmtpDeliveryMethod.Network;
|
||||
|
||||
await cliente.SendMailAsync(message, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Log the exception or handle it as needed
|
||||
Console.WriteLine($"Error sending email: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
9
back/services/engine/mailing/IEmailService.cs
Normal file
9
back/services/engine/mailing/IEmailService.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using DependencyInjector.Lifetimes;
|
||||
|
||||
namespace back.services.engine.mailing;
|
||||
|
||||
public interface IEmailService : IScoped
|
||||
{
|
||||
Task SendEmailAsync(List<string> tos, string from, string subject, string body, Dictionary<string, object>? attachments = null, CancellationToken cancellationToken = default);
|
||||
Task SendEmailAsync(string tos, string from, string subject, string body, Dictionary<string, object>? attachments = null, CancellationToken cancellationToken = default);
|
||||
}
|
Reference in New Issue
Block a user