diff --git a/Controllers/UsersController.cs b/Controllers/UsersController.cs index 3af3ce0..17f4ef7 100644 --- a/Controllers/UsersController.cs +++ b/Controllers/UsersController.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; +using System.Security.Claims; namespace line_gestao_api.Controllers; @@ -293,6 +294,95 @@ public class UsersController : ControllerBase return NoContent(); } + [HttpDelete("{id:guid}")] + [Authorize(Roles = "admin")] + public async Task Delete(Guid id) + { + if (_tenantProvider.TenantId == null) + { + return Unauthorized(); + } + + var currentUserId = GetCurrentUserId(); + if (currentUserId.HasValue && currentUserId.Value == id) + { + return BadRequest(new ValidationErrorResponse + { + Errors = new List + { + new() { Field = "usuario", Message = "Você não pode excluir a própria conta." } + } + }); + } + + var tenantId = _tenantProvider.TenantId.Value; + var user = await _userManager.Users.FirstOrDefaultAsync(u => u.Id == id && u.TenantId == tenantId); + if (user == null) + { + return NotFound(); + } + + if (user.IsActive) + { + return BadRequest(new ValidationErrorResponse + { + Errors = new List + { + new() { Field = "usuario", Message = "Inative a conta antes de excluir permanentemente." } + } + }); + } + + var targetRoles = await _userManager.GetRolesAsync(user); + var isAdmin = targetRoles.Any(r => string.Equals(r, "admin", StringComparison.OrdinalIgnoreCase)); + + if (isAdmin) + { + var adminRoleId = await _roleManager.Roles + .Where(r => r.Name == "admin") + .Select(r => (Guid?)r.Id) + .FirstOrDefaultAsync(); + + if (adminRoleId.HasValue) + { + var hasAnotherAdmin = await ( + from ur in _db.UserRoles + join u in _userManager.Users on ur.UserId equals u.Id + where ur.RoleId == adminRoleId.Value + && u.TenantId == tenantId + && u.Id != id + select u.Id + ).AnyAsync(); + + if (!hasAnotherAdmin) + { + return BadRequest(new ValidationErrorResponse + { + Errors = new List + { + new() { Field = "usuario", Message = "Não é permitido excluir o último administrador." } + } + }); + } + } + } + + var deleteResult = await _userManager.DeleteAsync(user); + if (!deleteResult.Succeeded) + { + return BadRequest(new ValidationErrorResponse + { + Errors = deleteResult.Errors.Select(e => new ValidationErrorDto + { + Field = "usuario", + Message = e.Description + }).ToList() + }); + } + + return NoContent(); + } + private static List ValidateCreate(UserCreateRequest req) { var errors = new List(); @@ -382,4 +472,10 @@ public class UsersController : ControllerBase return errors; } + + private Guid? GetCurrentUserId() + { + var raw = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub"); + return Guid.TryParse(raw, out var parsed) ? parsed : null; + } } diff --git a/Program.cs b/Program.cs index 291cc3d..5be119e 100644 --- a/Program.cs +++ b/Program.cs @@ -105,6 +105,7 @@ builder.Services.AddIdentityCore(options => options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(15); }) .AddRoles>() + .AddErrorDescriber() .AddEntityFrameworkStores() .AddDefaultTokenProviders(); diff --git a/Services/PortugueseIdentityErrorDescriber.cs b/Services/PortugueseIdentityErrorDescriber.cs new file mode 100644 index 0000000..ae05d9f --- /dev/null +++ b/Services/PortugueseIdentityErrorDescriber.cs @@ -0,0 +1,75 @@ +using Microsoft.AspNetCore.Identity; + +namespace line_gestao_api.Services; + +public class PortugueseIdentityErrorDescriber : IdentityErrorDescriber +{ + public override IdentityError DefaultError() + => NewError(nameof(DefaultError), "Ocorreu uma falha desconhecida."); + + public override IdentityError ConcurrencyFailure() + => NewError(nameof(ConcurrencyFailure), "Falha de concorrência. O registro foi alterado por outro processo."); + + public override IdentityError PasswordMismatch() + => NewError(nameof(PasswordMismatch), "Senha incorreta."); + + public override IdentityError InvalidToken() + => NewError(nameof(InvalidToken), "Token inválido."); + + public override IdentityError LoginAlreadyAssociated() + => NewError(nameof(LoginAlreadyAssociated), "Este login já está associado a outra conta."); + + public override IdentityError InvalidUserName(string? userName) + => NewError(nameof(InvalidUserName), $"Nome de usuário '{userName}' é inválido. Use apenas letras e números."); + + public override IdentityError InvalidEmail(string? email) + => NewError(nameof(InvalidEmail), $"E-mail '{email}' é inválido."); + + public override IdentityError DuplicateUserName(string userName) + => NewError(nameof(DuplicateUserName), $"O nome de usuário '{userName}' já está em uso."); + + public override IdentityError DuplicateEmail(string email) + => NewError(nameof(DuplicateEmail), $"O e-mail '{email}' já está em uso."); + + public override IdentityError InvalidRoleName(string? role) + => NewError(nameof(InvalidRoleName), $"O nome da permissão '{role}' é inválido."); + + public override IdentityError DuplicateRoleName(string role) + => NewError(nameof(DuplicateRoleName), $"A permissão '{role}' já existe."); + + public override IdentityError UserAlreadyHasPassword() + => NewError(nameof(UserAlreadyHasPassword), "Este usuário já possui senha definida."); + + public override IdentityError UserLockoutNotEnabled() + => NewError(nameof(UserLockoutNotEnabled), "Bloqueio não está habilitado para este usuário."); + + public override IdentityError UserAlreadyInRole(string role) + => NewError(nameof(UserAlreadyInRole), $"O usuário já possui a permissão '{role}'."); + + public override IdentityError UserNotInRole(string role) + => NewError(nameof(UserNotInRole), $"O usuário não possui a permissão '{role}'."); + + public override IdentityError PasswordTooShort(int length) + => NewError(nameof(PasswordTooShort), $"A senha deve ter pelo menos {length} caracteres."); + + public override IdentityError PasswordRequiresNonAlphanumeric() + => NewError(nameof(PasswordRequiresNonAlphanumeric), "A senha deve conter pelo menos um caractere especial."); + + public override IdentityError PasswordRequiresDigit() + => NewError(nameof(PasswordRequiresDigit), "A senha deve conter pelo menos um número ('0'-'9')."); + + public override IdentityError PasswordRequiresLower() + => NewError(nameof(PasswordRequiresLower), "A senha deve conter pelo menos uma letra minúscula ('a'-'z')."); + + public override IdentityError PasswordRequiresUpper() + => NewError(nameof(PasswordRequiresUpper), "A senha deve conter pelo menos uma letra maiúscula ('A'-'Z')."); + + public override IdentityError PasswordRequiresUniqueChars(int uniqueChars) + => NewError(nameof(PasswordRequiresUniqueChars), $"A senha deve usar pelo menos {uniqueChars} caracteres diferentes."); + + public override IdentityError RecoveryCodeRedemptionFailed() + => NewError(nameof(RecoveryCodeRedemptionFailed), "Código de recuperação inválido."); + + private static IdentityError NewError(string code, string description) + => new() { Code = code, Description = description }; +}