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

@@ -1,6 +0,0 @@
namespace back.services.ImageResizer;
public interface IImageResizer
{
Task<Stream> ResizeImage(IFormFile image, int v);
}

View File

@@ -0,0 +1,11 @@
using System.Net;
namespace back.services.bussines;
public static class Errors
{
public static readonly HttpErrorMap Unauthorized =
new(HttpStatusCode.Unauthorized, "Invalid user data. Email or password are wrong.");
public static readonly HttpErrorMap BadRequest =
new(HttpStatusCode.BadRequest, "Missing user data.");
}

View File

@@ -0,0 +1,5 @@
using System.Net;
namespace back.services.bussines;
public record HttpErrorMap(HttpStatusCode Code, string Description);

View File

@@ -0,0 +1,16 @@
using back.DataModels;
using back.DTO;
using DependencyInjector.Abstractions.ClassTypes;
using DependencyInjector.Lifetimes;
namespace back.services.bussines.PhotoService;
public interface IPhotoService: IScoped
{
Task<Photo?> Create(PhotoFormModel form);
Task Delete(string id, string userId = "00000000-0000-0000-0000-000000000001");
Task<Photo?> Get(string id, string userId = "00000000-0000-0000-0000-000000000001");
Task<(string? mediaType, byte[]? fileBytes)> GetBytes(string id, string res = "");
Task<(int totalItems, IEnumerable<Photo>? pageData)> GetPage(int page, int pageSize);
Task<Photo?> Update(Photo photo, string userId = "00000000-0000-0000-0000-000000000001");
}

View File

@@ -0,0 +1,82 @@
using back.DataModels;
using back.DTO;
using back.persistance.blob;
using back.persistance.data.repositories.Abstracts;
namespace back.services.bussines.PhotoService;
public class PhotoService(
IPhotoRepository photoRepository,
IUserRepository userRepository,
IBlobStorageService blobStorageService
) : IPhotoService
{
public async Task<Photo?> Create(PhotoFormModel form)
{
ArgumentNullException.ThrowIfNull(form);
if (form.Image == null || form.Image.Length == 0)
throw new ArgumentException("No image uploaded.", nameof(form));
//if (string.IsNullOrEmpty(form.UserId) || await userRepository.IsContentManager(form.UserId))
// throw new ArgumentException("Invalid user ID or user is not a content manager.", nameof(form.UserId));
throw new NotImplementedException();
}
public async Task Delete(string id, string userId = User.SystemUserId)
{
//if (string.IsNullOrEmpty(userId) || await userRepository.IsContentManager(userId))
// throw new ArgumentException("Invalid user ID or user is not a content manager.", nameof(userId));
photoRepository.Delete(id);
}
public async Task<Photo?> Get(string id, string userId = User.SystemUserId)
{
Photo? photo = await photoRepository.GetById(id);
return photo;
//return photo?.CanBeSeenBy(userId) ?? false
// ? photo
// : null;
}
public async Task<(string? mediaType, byte[]? fileBytes)> GetBytes(string id, string res = "")
{
var photo = await photoRepository.GetById(id);
if (photo == null)
return (null, null);
string filePath = res.ToLower() switch
{
"high" => photo.HighResUrl,
"mid" => photo.MidResUrl,
"low" or _ => photo.LowResUrl
};
string? mediaType = res.ToLower() switch
{
"high" => $"image/{photo.Extension}",
"mid" or "low" or _ => "image/webp",
};
return (
mediaType,
await blobStorageService.GetBytes(filePath) ?? throw new FileNotFoundException("File not found.", filePath)
);
}
public async Task<(int totalItems, IEnumerable<Photo>? pageData)> GetPage(int page, int pageSize)
{
return (
totalItems: await photoRepository.GetTotalItems(),
pageData: photoRepository.GetPage(page, pageSize)
);
}
public async Task<Photo?> Update(Photo photo, string userId = "00000000-0000-0000-0000-000000000001")
{
//if (string.IsNullOrEmpty(userId) || await userRepository.IsContentManager(userId))
// throw new ArgumentException("Invalid user ID or user is not a content manager.", nameof(userId));
return await photoRepository.Update(photo);
}
}

View File

@@ -0,0 +1,14 @@
using back.DataModels;
using DependencyInjector.Abstractions.ClassTypes;
using DependencyInjector.Lifetimes;
namespace back.services.bussines.UserService;
public interface IUserService: IScoped
{
Task<User?> Create(string clientId, User user);
Task<User?> Login(string email, string password, string clientId);
Task SendResetPassword(string email);
Task<User?> Update(User user);
Task<User?> ValidateSystemUser(string email, string password, string systemKey, string clientId);
}

View File

@@ -0,0 +1,133 @@
using back.DataModels;
using back.persistance.blob;
using back.persistance.data.repositories.Abstracts;
using back.services.engine.Crypto;
using back.services.engine.mailing;
using System.Text;
namespace back.services.bussines.UserService;
public class UserService(
IUserRepository userRepository, ICryptoService cryptoService,
IEmailService emailService,
IBlobStorageService blobStorageService
) : IUserService
{
private readonly IUserRepository _repository = userRepository ?? throw new ArgumentNullException(nameof(userRepository));
private readonly ICryptoService _cryptoService = cryptoService;
private readonly IEmailService _emailService = emailService;
private readonly IBlobStorageService _blobStorageService = blobStorageService;
public async Task<User?> Create(string clientId, User user)
{
ArgumentNullException.ThrowIfNull(user);
if (user.Id != null && await _repository.Exists(user.Id))
{
return await _repository.GetById(user.Id);
}
if (string.IsNullOrEmpty(user.Email) || string.IsNullOrEmpty(user.Password))
{
return null;
}
if (await _repository.Exists(user.Email))
{
return await _repository.GetByEmail(user.Email);
}
if (string.IsNullOrEmpty(user.Salt))
{
user.Salt = _cryptoService.Salt();
}
user.Password = _cryptoService.Decrypt(clientId, user.Password) ?? string.Empty;
user.Password = _cryptoService.HashPassword(user.Password, user.Salt) ?? string.Empty;
user.CreatedAt = DateTimeOffset.UtcNow.ToString("dd-MM-yyyy HH:mm:ss zz");
//user.Roles.Add(Role.UserRole);
await _repository.Insert(user);
await _repository.SaveChanges();
return user;
}
public async Task<User?> Update(User user)
{
ArgumentNullException.ThrowIfNull(user);
if (user.Id == null || !await _repository.Exists(user.Id))
{
return null;
}
var existingUser = await _repository.GetById(user.Id);
if (existingUser == null) return null;
existingUser.Email = user.Email;
await _repository.Update(existingUser);
await _repository.SaveChanges();
return existingUser;
}
public async Task<User?> Login(string email, string password, string clientId)
{
if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(password)) return null;
try
{
var decryptedPass = _cryptoService.Decrypt(clientId, password);
var salt = await _repository.GetUserSaltByEmail(email);
var hashedPassword = _cryptoService.HashPassword(decryptedPass, salt);
var user = await _repository.Login(email, hashedPassword ?? string.Empty);
return user;
}
catch
{
return null;
}
}
public async Task SendResetPassword(string email)
{
var exists = await _repository.ExistsByEmail(email);
if (!exists)
{
return;
}
await _emailService.SendEmailAsync(
tos: email,
from: "admin@mmorales.photo",
subject: "Reset Password",
body: "If you received this email, it means that you have requested a password reset. Please follow the instructions in the email to reset your password."
);
}
public async Task<User?> ValidateSystemUser(string email, string password, string systemKey, string clientId)
{
password = _cryptoService.Decrypt(clientId, password) ?? string.Empty;
systemKey = _cryptoService.Decrypt(clientId, systemKey) ?? string.Empty;
if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(password) || string.IsNullOrEmpty(systemKey))
{
return null;
}
if (!email.Equals("@system", StringComparison.InvariantCultureIgnoreCase))
{
return null;
}
var systemKeyBytes = await _blobStorageService.GetBytes("systemkey.lock");
var systemKeyString = Encoding.UTF8.GetString(systemKeyBytes ?? []);
var systemKeyObject = System.Text.Json.JsonSerializer.Deserialize<SystemKey>(systemKeyString);
if (systemKeyObject == null || !systemKeyObject.IsValid(email, password, systemKey))
{
return null;
}
if (!await _repository.ExistsByEmail(email))
{
return null;
}
var user = await _repository.GetByEmail(email);
if (user == null)
{
return null;
}
return await Login(user.Email!, user.Password!, clientId);
}
}

View File

@@ -1,7 +1,7 @@
using Microsoft.Extensions.Caching.Memory;
using System.Security.Cryptography;
namespace back.services.Crypto;
namespace back.services.engine.Crypto;
public class CryptoService(IMemoryCache cache) : ICryptoService
{
@@ -28,7 +28,7 @@ public class CryptoService(IMemoryCache cache) : ICryptoService
}
};
public string? Encrypt(string plainText, string clientId)
public string? Encrypt(string clientId,string plainText)
{
// get keys from cache
if (!_cache.TryGetValue($"{clientId}_private", out string? privateCert) || string.IsNullOrEmpty(privateCert))
@@ -41,7 +41,7 @@ public class CryptoService(IMemoryCache cache) : ICryptoService
}
// import rsa keys and configure RSA for encryption
using var rsa = RSA.Create(2048);
rsa.ImportRSAPublicKey(Convert.FromBase64String(publicCert), out _);
rsa.ImportSubjectPublicKeyInfo(Convert.FromBase64String(publicCert), out _);
rsa.ImportRSAPrivateKey(Convert.FromBase64String(privateCert), out _);
// Encrypt the plain text using RSA
string? encryptedText = null;
@@ -59,7 +59,7 @@ public class CryptoService(IMemoryCache cache) : ICryptoService
return encryptedText;
}
public string? Decrypt(string encryptedText, string clientId)
public string? Decrypt(string clientId, string encryptedText)
{
// get keys from cache
if (!_cache.TryGetValue($"{clientId}_private", out string? privateCert) || string.IsNullOrEmpty(privateCert))
@@ -72,7 +72,7 @@ public class CryptoService(IMemoryCache cache) : ICryptoService
}
// import rsa keys and configure RSA for decryption
using var rsa = RSA.Create(2048);
rsa.ImportRSAPublicKey(Convert.FromBase64String(publicCert), out _);
rsa.ImportSubjectPublicKeyInfo(Convert.FromBase64String(publicCert), out _);
rsa.ImportRSAPrivateKey(Convert.FromBase64String(privateCert), out _);
// Decrypt the encrypted text using RSA
string? plainText = null;
@@ -96,8 +96,9 @@ public class CryptoService(IMemoryCache cache) : ICryptoService
{
return publicCert;
}
(publicCert, _) = GenerateCertificate();
(publicCert, string privateCert) = GenerateCertificate();
_cache.Set($"{clientId}_public", publicCert, _CacheOptions);
_cache.Set($"{clientId}_private", privateCert, _CacheOptions);
return publicCert;
}
@@ -107,7 +108,8 @@ public class CryptoService(IMemoryCache cache) : ICryptoService
{
return privateCert;
}
(_, privateCert) = GenerateCertificate();
(string publicCert, privateCert) = GenerateCertificate();
_cache.Set($"{clientId}_public", publicCert, _CacheOptions);
_cache.Set($"{clientId}_private", privateCert, _CacheOptions);
return privateCert;
}
@@ -116,7 +118,7 @@ public class CryptoService(IMemoryCache cache) : ICryptoService
{
// Generate a new RSA key pair for the client
using var rsa = RSA.Create(2048);
var publicKey = rsa.ExportRSAPublicKey();
var publicKey = rsa.ExportSubjectPublicKeyInfo();
var privateKey = rsa.ExportRSAPrivateKey();
// Convert to Base64 strings for storage
var publicCert = Convert.ToBase64String(publicKey);
@@ -165,4 +167,9 @@ public class CryptoService(IMemoryCache cache) : ICryptoService
rng.GetBytes(saltBytes);
return Convert.ToBase64String(saltBytes);
}
public string? HashPassword(string plainPassword, string plainSalt)
{
return Hash($"{plainPassword}{plainSalt}{Pepper()}");
}
}

View File

@@ -1,10 +1,13 @@
namespace back.services.Crypto;
using DependencyInjector.Lifetimes;
public interface ICryptoService
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();

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

@@ -1,7 +1,7 @@
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Processing;
namespace back.services.ImageResizer;
namespace back.services.engine.ImageResizer;
public sealed class ImageResizer : IImageResizer
{

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