Creado apartir del commit b888de6c92056de294456f581a56e25e734d4ab7 de develop

This commit is contained in:
jrojas
2026-06-26 09:52:35 +02:00
commit 319fd3dfb0
746 changed files with 106610 additions and 0 deletions
+647
View File
@@ -0,0 +1,647 @@
using System.Diagnostics;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using adas_core.Application.Exceptions;
using adas_core.Application.Repositories.Interfaces;
using adas_core.Application.Services.Interfaces;
using adas_core.Authentication.Interfaces;
using adas_core.Authentication.Models;
using adas_core.Domain.Enums;
using adas_core.Domain.Exceptions;
using adas_core.Domain.Models.AppSettings;
using adas_core.Domain.Models.DTO;
using adas_core.Domain.Models.Filter;
using adas_core.Domain.Models.MongoModels;
using adas_core.Domain.Models.Responses;
using adas_core.Domain.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using MongoDB.Bson;
using MongoDB.Driver;
using static System.Enum;
namespace adas_core.Authentication;
public class UserService : IUserService
{
private readonly ILocalAuditService _auditService;
private readonly IAuthorityService _authorityService;
private readonly IEnumerable<ILoginService> _availableLoginServices;
private readonly Lazy<IDisplayService> _displayService;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly JwtConfig _jwtConfig;
private readonly ILogger<UserService> _logger;
private readonly IUserRepository _userRepository;
private readonly UsersWhiteListConfig _usersWhiteList;
private readonly ValidGroupsConfig _validGroups;
private readonly List<string> _validLoginMethods;
private readonly ISubscribersService _subscribersService;
private readonly IClientMessageService _clientMessageService;
private readonly Lazy<IPermissionService> _permissionService;
public UserService(
IEnumerable<ILoginService> loginServices,
IOptions<AuthSettings> configuration,
IOptions<ValidGroupsConfig> validGroups,
IOptions<AuthSettings> usersWhiteList,
IOptions<AuthSettings> jwt,
IHttpContextAccessor httpContextAccessor,
IUserRepository userRepository,
IAuthorityService authorityService,
ILocalAuditService auditService,
Lazy<IDisplayService> displayService,
ILogger<UserService> logger,
ISubscribersService subscribersService,
IClientMessageService clientMessageService, Lazy<IPermissionService> permissionService)
{
_availableLoginServices = loginServices;
_httpContextAccessor = httpContextAccessor;
_validGroups = validGroups.Value;
_usersWhiteList = usersWhiteList.Value.UsersWhiteListConfig;
_jwtConfig = jwt.Value.JwtConfig ?? throw new Exception("JWT NOT CONFIGURED");
var loginMethods = configuration.Value.LoginMethods;
_validLoginMethods = loginMethods;
_userRepository = userRepository;
_authorityService = authorityService;
_auditService = auditService;
_displayService = displayService;
_logger = logger;
_subscribersService = subscribersService;
_clientMessageService = clientMessageService;
_permissionService = permissionService;
}
public async Task<TokenResult> Login(string username, string password)
{
var user = await GetUser(username, password);
_logger.LogInformation("[UserService] Login user {u}", user.ToString());
if (user == null) throw new UnauthorizedException(HttpEnum.ErrorMessage.UnauthorizedInvalidCredentials);
if (!user.IsEnabled || (user.LockExpirationDate != null && user.LockExpirationDate > DateTime.UtcNow))
throw new ForbbidenException(HttpEnum.ErrorMessage.ForbiddenUserBlocked);
if (!_usersWhiteList.IsValid(user) && !_validGroups.IsValid(user))
throw new UnauthorizedException(HttpEnum.ErrorMessage.UnauthorizedNoPermission);
if (user.LockExpirationDate < DateTime.UtcNow) user.LockExpirationDate = null;
user.LastLogin = DateTime.UtcNow;
await UpdateUsersByRequest(user, false);
return await GenerateJwt(user);
}
public void ValidateToken(string token, string tokenType, out SecurityToken validatedToken)
{
try
{
var tokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = _jwtConfig.ValidateAudience,
ValidateIssuer = _jwtConfig.ValidateIssuer,
ValidAudience = _jwtConfig.Audience,
ValidIssuer = _jwtConfig.Issuer,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtConfig.Key ?? string.Empty)),
ValidateLifetime = false
};
var tokenHandler = new JwtSecurityTokenHandler();
tokenHandler.ValidateToken(token, tokenValidationParameters, out var securityToken);
if (securityToken is not JwtSecurityToken jwtSecurityToken ||
!jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256,
StringComparison.InvariantCultureIgnoreCase))
throw new SecurityTokenException("Invalid token");
validatedToken = securityToken;
}
catch (SecurityTokenException a)
{
_logger.LogError("Error validating token, {msg}", a.Message);
throw new TokenException(a.Message);
}
catch (Exception e)
{
_logger.LogError("Error validating token, {msg}", e.Message);
throw new TokenException("Error validating token");
}
}
public async Task<User?> GetUserByToken(JwtSecurityToken? jwtToken)
{
User? user = null;
if (jwtToken == null)
return user;
var userName = jwtToken.Claims.First(x => x.Type == ClaimTypes.Name).Value;
user = await _userRepository.GetByUserName(userName);
if (user == null)
return user;
//Eliminamos la contraseña
user.Password = string.Empty;
if (user.Authorization == null || user.Authorization.Count == 0)
user.Authorization = await _authorityService.GetUserAuthorities(user.Id);
return user;
}
public async Task<User?> GetUserByCasTicket(string serviceUrl, string ticket)
{
var service = _availableLoginServices.FirstOrDefault(s => s.Method == UserEnum.LoginMethod.Cas);
if (service == null) throw new LoginServicesException("CAS login method not available");
try
{
var user = await service.Login(_httpContextAccessor.HttpContext!);
if (!_usersWhiteList.IsValid(user) && !_validGroups.IsValid(user))
throw new UnauthorizedAccessException("You don't have sufficient permissions");
return user;
}
catch (BusinessException)
{
throw;
}
catch (UnauthorizedAccessException)
{
throw;
}
catch (Exception e)
{
var message = e.Message;
if (e.InnerException != null && e.Message != e.InnerException?.Message)
message += $", {e.InnerException?.Message}";
Debug.WriteLine($"Error while login: {message} ");
throw new LoginServicesException(message, e);
}
////////////////////////
/*
if (!_validLoginMethods.Contains(LoginMethod.CAS.ToString()))
{
Log.Warning("CAS login method not enabled");
return null;
}
_casLoginService = new
Log.Information("CAS ticket received: '{CasTicket}'", ticket);
return string.IsNullOrEmpty(ticket) ? null : _casLoginService.GetUser(serviceUrl, ticket, context);
*/
}
public async Task<TokenResult> RefreshToken(string refreshToken)
{
ValidateToken(refreshToken, IUserService.TokenTypeRefresh, out var validatedToken);
var user = await GetUserByToken((JwtSecurityToken)validatedToken);
if (user == null)
return new TokenResult();
return await GenerateJwt(user);
}
public async Task<User> GetUser(string username, string password)
{
//BusinessException? loginException = null;
foreach (var m in _validLoginMethods)
{
if (!TryParse(m, out UserEnum.LoginMethod method)) continue;
var service = _availableLoginServices.FirstOrDefault(s => s.Method == method && s.AllowPassword);
if (service == null) continue;
try
{
_logger.LogInformation("[UserService] Trying login with method {method}", method);
return await service.Login(username, password);
}
catch (Exception e)
{
_logger.LogError("[UserService] Error while trying method {method}, err {err}", method, e.Message);
}
}
throw new LoginServicesNotFoundException();
}
public async Task<User?> GetUserById(ObjectId id)
{
try
{
var u = await _userRepository.GetById(id);
if (u != null) u.Authorization = await _authorityService.GetUserAuthorities(u.Id);
return u;
}
catch (Exception)
{
return null;
}
}
public async Task<User?> GetUserByUserName(string name)
{
var u = await _userRepository.GetByUserName(name);
if (u == null) return u;
u.Authorization = await _authorityService.GetUserAuthorities(u.Id);
return u;
}
public async Task<User?> GetUserByName(string name)
{
try
{
var u = await _userRepository.GetByName(name);
if (u != null) u.Authorization = await _authorityService.GetUserAuthorities(u.Id);
return u;
}
catch (Exception)
{
return null;
}
}
public async Task<User?> CreateUser(User userEntryLdap)
{
CheckIfUserNameExists(userEntryLdap.UserName);
CheckIfEmailExists(userEntryLdap.Email);
await _userRepository.InsertOneAsync(userEntryLdap);
var result = await GetUserByUserName(userEntryLdap.UserName);
await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User, null, result);
return result;
}
public async Task<User?> CreateNewUserByRequest(User user)
{
CheckIfEmailExists(user.Email);
CheckIfUserNameExists(user.UserName);
if (user.Type == UserEnum.Type.Local)
{
if (!CryptoAdas.IsStrongPassword(user.Password))
throw new UnprocessableEntityException(HttpEnum.ErrorMessage.UnprocessableEntityInvalidPassword);
user.Password = CryptoAdas.CreateBCrypt(user.Password);
}
await _userRepository.InsertOneAsync(user);
var result = await GetUserByUserName(user.UserName) ??
throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User, null, result);
return result;
}
public async Task<User?> CreateNewUserWithAuthorities(CreateUserWithAuthDto createUserWithAuthDto)
{
var user = createUserWithAuthDto.User;
var newUser = await CreateNewUserByRequest(user) ??
throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
var auths = createUserWithAuthDto.Authorizations;
foreach (var auth in auths)
{
auth.UserId = newUser.Id;
await CreateNewAuthority(auth);
}
return newUser;
}
public async Task<User?> UpdateUsersByRequest(User user, bool updatePass)
{
var oldUser = await _userRepository.GetByUserName(user.UserName);
var logoutHasBeenSent = false;
if (updatePass && !CryptoAdas.IsStrongPassword(user.Password))
throw new UnprocessableEntityException(HttpEnum.ErrorMessage.UnprocessableEntityInvalidPassword);
if (updatePass)
{
user.Password = CryptoAdas.CreateBCrypt(user.Password);
await SendBroadcastUser(user, user.UserName, OperationType.UpdatePassword);
logoutHasBeenSent = true;
}
var result = await _userRepository.UpdateUser(user, updatePass);
if(!logoutHasBeenSent && oldUser != null && result != null && oldUser.IsEnabled && !result.IsEnabled)
{
await SendBroadcastUser(result, user.UserName, OperationType.DisableUser);
logoutHasBeenSent = true;
}
if(!logoutHasBeenSent && result != null && result.LockExpirationDate != null && result.LockExpirationDate>DateTime.UtcNow) await SendBroadcastUser(result, user.UserName, OperationType.LockUser);
await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User, oldUser, result);
return result;
}
private async Task SendBroadcastUser(object elementUpdated, string userName, OperationType operation)
{
var subscribers = _subscribersService.GetSubscribers().Where(s =>
s.UserName == userName).ToList();
foreach (var subscriber in subscribers)
{
await _clientMessageService.SendAsync(subscriber.Id, operation, elementUpdated);
}
}
private async Task SendBroadcastPermissions(object elementUpdated, string userName, OperationType operation)
{
var subscribers = _subscribersService.GetSubscribers().Where(s =>
s.UserName == userName).ToList();
foreach (var subscriber in subscribers)
{
if (subscriber.SubscriptionType == SubscriptionEnum.WsType.Admin)
{
try
{
var permissionsForPanel = await _permissionService.Value.GetPermissionsForPanel(userName);
_ = _clientMessageService.SendAsync(subscriber.Id, operation, permissionsForPanel);
}
catch (Exception)
{
_ = _clientMessageService.SendAsync(subscriber.Id, operation, null);
}
}
else
{
_ = _clientMessageService.SendAsync(subscriber.Id, operation, elementUpdated);
}
}
}
public async Task<User?> UpdateUserWithAuthorities(UpdateUserWithAuthDto updateUserWithAuthDto, bool updatePass)
{
var u = updateUserWithAuthDto.User;
CheckIfEmailExists(u.Email, u.Id);
CheckIfUserNameExists(u.UserName, u.Id);
var userUpdated = await UpdateUsersByRequest(u, updatePass) ??
throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed);
foreach (var id in updateUserWithAuthDto.AuthsToDelete) await DeleteAuthority(id);
foreach (var auth in updateUserWithAuthDto.Authorizations) await UpdateAuthority(auth);
foreach (var auth in updateUserWithAuthDto.NewAuthorizations)
{
auth.UserId = userUpdated.Id;
await CreateNewAuthority(auth);
}
if(updateUserWithAuthDto.AuthsToDelete.Any() || updateUserWithAuthDto.NewAuthorizations.Any() || updateUserWithAuthDto.Authorizations.Any())
{
var allPermissionsForDisplay = await _displayService.Value.GetAllByUser(u.UserName);
await SendBroadcastPermissions(allPermissionsForDisplay, u.UserName, OperationType.UpdateAuthorities);
}
return userUpdated;
}
public async Task<bool> UpdateUserPassword(ObjectId id, string oldPassword, string newPassword)
{
if (oldPassword == newPassword)
throw new BadRequestException("New password must be different");
var user = await _userRepository.GetById(id) ??
throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
if (!CryptoAdas.VerifyPassword(oldPassword, user.Password))
throw new UnprocessableEntityException("Invalid current password");
if (!CryptoAdas.IsStrongPassword(newPassword))
throw new UnprocessableEntityException("Weak password");
user.Password = CryptoAdas.CreateBCrypt(newPassword);
var result = await _userRepository.UpdateUser(user, true);
if (result != null)
{
_logger.LogInformation("Password updated for user {UserId}", id);
await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User, user, result);
return true;
}
return false;
}
public async Task<bool> DeleteUser(ObjectId id)
{
try
{
var user = await _userRepository.GetById(id);
await _userRepository.DeleteAsync(id);
await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User, user, null);
return true;
}
catch (Exception e)
{
_logger.LogError("Error deleting user. Exception: {e}", e);
return false;
}
}
public async Task<PaginationResponse<User>> GetPaginatedUsers(PaginationFilter filter)
{
var result = _userRepository.GetPaginatedUsers(filter);
var count = await result.CountDocumentsAsync();
var data = await result.Skip((filter.PageNumber - 1) * filter.PageSize)
.Limit(filter.PageSize)
.ToCursorAsync();
var dataList = await data.ToListAsync();
return new PaginationResponse<User>(dataList, filter.PageNumber, filter.PageSize, count);
}
public async Task<TokenResult> GenerateJwt(User user)
{
var claims = new List<Claim>
{
new(ClaimTypes.Name, user.UserName),
new(ClaimTypes.Email, user.Email),
new("IpAddress", user.IpAddress)
};
// Agregar claims de autorizaciones utilizando ClaimsFromAuthorities
if (user.Authorization != null)
claims.AddRange(await ClaimsFromAuthorities(user.Authorization));
var accessToken = JwtHelper.GetJwtToken(
user.UserName,
_jwtConfig.Key ?? string.Empty,
_jwtConfig.Issuer ?? string.Empty,
_jwtConfig.Audience ?? string.Empty,
_jwtConfig.TokenValidityInMinutes,
claims.ToArray());
var refreshToken = JwtHelper.GetJwtToken(
user.UserName,
_jwtConfig.Key ?? string.Empty,
_jwtConfig.Issuer ?? string.Empty,
_jwtConfig.Audience ?? string.Empty,
_jwtConfig.RefreshTokenValidityInMinutes,
claims.ToArray());
return new TokenResult
{
AccessToken = new JwtSecurityTokenHandler().WriteToken(accessToken),
RefreshToken = new JwtSecurityTokenHandler().WriteToken(refreshToken),
AccessTokenExpiryTime = accessToken.ValidTo,
RefreshTokenExpiryTime = refreshToken.ValidTo,
User = user
};
}
public async Task<List<User>> GetAll()
{
BusinessException? loginException = null;
foreach (var m in _validLoginMethods)
{
if (!TryParse(m, out UserEnum.LoginMethod method)) continue;
var service = _availableLoginServices.FirstOrDefault(s => s.Method == method && s.AllowPassword);
if (service == null) continue;
try
{
return await service.GetAllUsers();
}
catch (BusinessException e)
{
loginException = e;
}
catch (Exception e)
{
var message = e.Message;
if (e.Message != e.InnerException?.Message) message += $", {e.InnerException?.Message}";
Debug.WriteLine($"Error Getting all users: {message} ");
loginException = new LoginServicesException(message);
}
}
if (loginException != null) throw loginException;
throw new LoginServicesNotFoundException();
}
public async Task<TokenResult> LoginWithGivenAccessToken(string token)
{
ValidateToken(token, IUserService.TokenTypeUser, out var validatedToken);
var jwtToken = (JwtSecurityToken)validatedToken;
var user = await GetUserByToken(jwtToken) ??
throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
user.Authorization ??= await _authorityService.GetUserAuthorities(user.Id);
if (jwtToken.ValidTo < DateTime.UtcNow)
return await GenerateJwt(user);
var claims = new List<Claim>
{
new(ClaimTypes.Name, user.UserName),
new(ClaimTypes.Email, user.Email),
new("IpAddress", user.IpAddress)
};
// Agregar claims de autorizaciones utilizando ClaimsFromAuthorities
claims.AddRange(await ClaimsFromAuthorities(user.Authorization));
var dif = jwtToken.ValidTo > DateTime.Now ? jwtToken.ValidTo - DateTime.Now : DateTime.Now - jwtToken.ValidTo;
var accessToken = JwtHelper.GetJwtToken(
user.UserName,
_jwtConfig.Key ?? string.Empty,
_jwtConfig.Issuer ?? string.Empty,
_jwtConfig.Audience ?? string.Empty,
(int)dif.TotalMinutes,
claims.ToArray());
var refreshToken = JwtHelper.GetJwtToken(
user.UserName,
_jwtConfig.Key ?? string.Empty,
_jwtConfig.Issuer ?? string.Empty,
_jwtConfig.Audience ?? string.Empty,
(int)dif.TotalMinutes + _jwtConfig.RefreshTokenValidityInMinutes,
claims.ToArray());
return new TokenResult
{
AccessToken = new JwtSecurityTokenHandler().WriteToken(accessToken),
RefreshToken = new JwtSecurityTokenHandler().WriteToken(refreshToken),
AccessTokenExpiryTime = accessToken.ValidTo,
RefreshTokenExpiryTime = refreshToken.ValidTo,
User = user
};
}
public async Task<TokenResult> LoginWithAccessToken(string token)
{
ValidateToken(token, IUserService.TokenTypeUser, out var validatedToken);
var jwtToken = (JwtSecurityToken)validatedToken;
var user = await GetUserByToken(jwtToken) ??
throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
user.Authorization ??= await _authorityService.GetUserAuthorities(user.Id);
return await GenerateJwt(user);
}
public async Task<Authorization> CreateNewAuthority(Authorization auth)
{
var newUserAuthority = await _authorityService.CreateNewUserAuthority(auth) ??
throw new ConflictException(HttpEnum.ErrorMessage.ConflictCreationFailed);
return newUserAuthority;
}
public async Task<bool> DeleteAuthority(string id)
{
if (!ObjectId.TryParse(id, out var userAuthorityIdParsed))
throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat);
var isDeleted = await _authorityService.DeleteUserAuthority(userAuthorityIdParsed);
return !isDeleted ? throw new ConflictException(HttpEnum.ErrorMessage.ConflictDeleteFailed) : isDeleted;
}
public async Task<bool> UpdateAuthority(Authorization authorization)
{
_ = await _authorityService.EditUserAuthority(authorization) ??
throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed);
return true;
}
public async Task<List<Claim>> ClaimsFromAuthorities(List<Authorization> authorities)
{
var claims = new List<Claim>();
foreach (var auth in authorities)
if (auth.UnitId != null)
{
var dis = await
_displayService.Value.GetByUnitId(ObjectId.Parse(auth.UnitId));
foreach (var claim in dis)
{
auth.DisplayId = claim.Id.ToString();
AddClaim(auth, claims);
}
}
else
{
AddClaim(auth, claims);
}
return claims;
}
private static void AddClaim(Authorization auth, List<Claim> claims)
{
var newClaim = new Claim(ClaimTypes.Role,
auth.Rol + (string.IsNullOrEmpty(auth.DisplayId) ? "" : "_" + auth.DisplayId));
if (claims.Any(c => c.Value == newClaim.Value)) return;
//newClaim.Properties.Add("DisplayId", auth.DisplayId ?? "");
claims.Add(newClaim);
}
private void CheckIfUserNameExists(string userName, ObjectId? userId = null)
{
var users = GetAll().Result;
var userNameExists = userId.HasValue
? users.Any(user => user.Id != userId.Value && user.UserName == userName)
: users.Any(user => user.UserName == userName);
if (userNameExists) throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestIncorrectEmailOrUsername);
}
private void CheckIfEmailExists(string email, ObjectId? userId = null)
{
var users = GetAll().Result;
var emailExists = userId.HasValue
? users.Any(user => user.Id != userId.Value && user.Email == email)
: users.Any(user => user.Email == email);
if (emailExists && !string.IsNullOrEmpty(email))
throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestIncorrectEmailOrUsername);
}
}