transactions

This commit is contained in:
2025-08-24 14:18:20 +02:00
parent 1b2d95344a
commit 5777e351bf
107 changed files with 4940 additions and 1266 deletions

View 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()}");
}
}

View 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);
}

View File

@@ -0,0 +1,8 @@
using DependencyInjector.Lifetimes;
namespace back.services.engine.ImageResizer;
public interface IImageResizer : ISingleton
{
Task<Stream> ResizeImage(IFormFile image, int v);
}

View 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;
}
}

View File

@@ -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);
}

View 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);
}
}

View File

@@ -0,0 +1,8 @@
using DependencyInjector.Lifetimes;
namespace back.services.engine.SystemUser;
public interface ISystemUserGenerator: IScoped
{
Task GenerateAsync();
}

View 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);
});
}
}
}

View 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}");
}
}
}

View 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);
}