Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/Voltiq.API/Controllers/Auth/AuthController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public async Task<IActionResult> Login(
var command = request.ToCommand();
var result = await Sender.Send(command, cancellationToken);

return result.IsFailure ? ToErrorResult(result) : Ok(result.Value);
return result.IsError ? ToErrorResult(result) : Ok(result.Value);
}

/// <summary>Exchanges a valid refresh token for a new access token and refresh token (rotation).</summary>
Expand All @@ -43,7 +43,7 @@ public async Task<IActionResult> Refresh(
{
var result = await Sender.Send(request.ToCommand(), cancellationToken);

return result.IsFailure ? ToErrorResult(result) : Ok(result.Value);
return result.IsError ? ToErrorResult(result) : Ok(result.Value);
}

/// <summary>Returns the currently authenticated user.</summary>
Expand All @@ -58,6 +58,6 @@ public async Task<IActionResult> Me(CancellationToken cancellationToken)
{
var result = await Sender.Send(new GetCurrentUserQuery(), cancellationToken);

return result.IsFailure ? ToErrorResult(result) : Ok(result.Value);
return result.IsError ? ToErrorResult(result) : Ok(result.Value);
}
}
35 changes: 17 additions & 18 deletions src/Voltiq.API/Controllers/BaseApiController.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
using System.Diagnostics.CodeAnalysis;
using ErrorOr;
using MediatR;
using Microsoft.AspNetCore.Mvc;
using Voltiq.Domain.Common;
using Voltiq.Exceptions.Errors;
using Voltiq.Exceptions.Resources;

namespace Voltiq.API.Controllers;
Expand All @@ -15,29 +14,29 @@ public abstract class BaseApiController : ControllerBase
protected ISender Sender =>
field ??= HttpContext.RequestServices.GetRequiredService<ISender>();

protected IActionResult ToErrorResult(Result result)
protected IActionResult ToErrorResult<T>(ErrorOr<T> result)
{
var firstError = result.FirstError;

return firstError switch
return firstError.Type switch
{
ValidationError => ValidationProblem(firstError, result),
ErrorType.Validation => BuildValidationProblem(result.Errors),

NotFoundError notFound => Problem(
ErrorType.NotFound => Problem(
title: ResourceErrorMessages.TITULO_NAO_ENCONTRADO,
detail: notFound.Message,
detail: firstError.Description,
statusCode: StatusCodes.Status404NotFound,
instance: HttpContext.Request.Path),

ConflictError conflict => Problem(
ErrorType.Conflict => Problem(
title: ResourceErrorMessages.TITULO_CONFLITO,
detail: conflict.Message,
detail: firstError.Description,
statusCode: StatusCodes.Status409Conflict,
instance: HttpContext.Request.Path),

UnauthorizedError unauthorized => Problem(
ErrorType.Unauthorized => Problem(
title: ResourceErrorMessages.TITULO_NAO_AUTORIZADO,
detail: unauthorized.Message,
detail: firstError.Description,
statusCode: StatusCodes.Status401Unauthorized,
instance: HttpContext.Request.Path),

Expand All @@ -48,19 +47,19 @@ protected IActionResult ToErrorResult(Result result)
};
}

private ObjectResult ValidationProblem(Error _, Result result)
private ObjectResult BuildValidationProblem(List<Error> errors)
{
var errors = new Dictionary<string, string[]>();
var dict = new Dictionary<string, string[]>();

foreach (var error in result.Errors.OfType<ValidationError>())
foreach (var error in errors.Where(e => e.Type == ErrorType.Validation))
{
if (!errors.ContainsKey(error.PropertyName))
errors[error.PropertyName] = [];
if (!dict.ContainsKey(error.Code))
dict[error.Code] = [];

errors[error.PropertyName] = [.. errors[error.PropertyName], error.Message];
dict[error.Code] = [.. dict[error.Code], error.Description];
}

var problemDetails = new ValidationProblemDetails(errors)
var problemDetails = new ValidationProblemDetails(dict)
{
Title = ResourceErrorMessages.TITULO_FALHA_VALIDACAO,
Status = StatusCodes.Status400BadRequest,
Expand Down
2 changes: 1 addition & 1 deletion src/Voltiq.API/Controllers/Users/UsersController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public async Task<IActionResult> Create(
var command = request.ToCommand();
var result = await Sender.Send(command, cancellationToken);

return result.IsFailure ?
return result.IsError ?
ToErrorResult(result) :
CreatedAtAction(nameof(Create), new { id = result.Value.Id }, result.Value);
}
Expand Down
1 change: 1 addition & 0 deletions src/Voltiq.API/Voltiq.API.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="ErrorOr" Version="2.0.1" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
19 changes: 8 additions & 11 deletions src/Voltiq.Application/Common/Behaviors/ValidationBehavior.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
using System.Reflection;
using ErrorOr;
using FluentValidation;
using MediatR;
using Voltiq.Domain.Common;
using Voltiq.Exceptions.Errors;

namespace Voltiq.Application.Common.Behaviors;

public sealed class ValidationBehavior<TRequest, TResponse>(IEnumerable<IValidator<TRequest>> validators)
: IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
where TResponse : Result
where TResponse : IErrorOr
{
public async Task<TResponse> Handle(
TRequest request,
Expand All @@ -30,7 +30,7 @@ public async Task<TResponse> Handle(
return await next(cancellationToken);

var errors = failures
.Select(Error (f) => new ValidationError(f.PropertyName, f.ErrorMessage))
.Select(f => Error.Validation(code: f.PropertyName, description: f.ErrorMessage))
.ToList();

return CreateFailure(errors);
Expand All @@ -40,17 +40,14 @@ private static TResponse CreateFailure(List<Error> errors)
{
var responseType = typeof(TResponse);

if (responseType == typeof(Result))
return (TResponse)Result.Failure(errors);

if (responseType.IsGenericType && responseType.GetGenericTypeDefinition() == typeof(Result<>))
if (responseType.IsGenericType && responseType.GetGenericTypeDefinition() == typeof(ErrorOr<>))
{
var typeArg = responseType.GetGenericArguments()[0];
var failureMethod = typeof(Result<>)
var implicitOp = typeof(ErrorOr<>)
.MakeGenericType(typeArg)
.GetMethod(nameof(Result<object>.Failure), [typeof(IReadOnlyList<Error>)])!;
.GetMethod("op_Implicit", BindingFlags.Static | BindingFlags.Public, [typeof(List<Error>)])!;

return (TResponse)failureMethod.Invoke(null, [errors.AsReadOnly()])!;
return (TResponse)implicitOp.Invoke(null, [errors])!;
}

throw new InvalidOperationException($"Unexpected TResponse type: {responseType}");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using ErrorOr;
using MediatR;
using Voltiq.Domain.Common;

namespace Voltiq.Application.Features.Auth.Commands.Login;

public sealed record LoginCommand(string Email, string Password) : IRequest<Result<LoginResponse>>;
public sealed record LoginCommand(string Email, string Password) : IRequest<ErrorOr<LoginResponse>>;
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
using ErrorOr;
using MediatR;
using Voltiq.Application.Common.Interfaces;
using Voltiq.Domain.Common;
using Voltiq.Domain.Entities;
using Voltiq.Domain.Interfaces;
using Voltiq.Domain.Interfaces.Repositories;
using Voltiq.Domain.Interfaces.Repositories.User;
using Voltiq.Domain.ValueObjects;
using Voltiq.Exceptions.Errors;
using Voltiq.Exceptions.Resources;

namespace Voltiq.Application.Features.Auth.Commands.Login;
Expand All @@ -17,17 +16,16 @@ public sealed class LoginCommandHandler(
ITokenService tokenService,
IRefreshTokenRepository refreshTokenRepository,
IUnitOfWork unitOfWork)
: IRequestHandler<LoginCommand, Result<LoginResponse>>
: IRequestHandler<LoginCommand, ErrorOr<LoginResponse>>
{
public async Task<Result<LoginResponse>> Handle(LoginCommand request, CancellationToken cancellationToken)
public async Task<ErrorOr<LoginResponse>> Handle(LoginCommand request, CancellationToken cancellationToken)
{
var email = Email.Create(request.Email).Value;

var user = await userRepository.GetByEmailAsync(email, cancellationToken);

if (user is null || !passwordHasher.Verify(request.Password, user.PasswordHash))
return Result<LoginResponse>.Failure(
new UnauthorizedError(ResourceErrorMessages.LOGIN_CREDENCIAIS_INVALIDAS));
return Error.Unauthorized(description: ResourceErrorMessages.LOGIN_CREDENCIAIS_INVALIDAS);

var accessToken = tokenService.GenerateAccessToken(user.Id.ToString(), user.Name, []);
var rawRefreshToken = tokenService.GenerateRefreshToken();
Expand All @@ -36,6 +34,6 @@ public async Task<Result<LoginResponse>> Handle(LoginCommand request, Cancellati
await refreshTokenRepository.AddAsync(refreshToken, cancellationToken);
await unitOfWork.SaveChangesAsync(cancellationToken);

return Result<LoginResponse>.Success(new LoginResponse(accessToken, rawRefreshToken));
return new LoginResponse(accessToken, rawRefreshToken);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using ErrorOr;
using MediatR;
using Voltiq.Domain.Common;

namespace Voltiq.Application.Features.Auth.Commands.Refresh;

public sealed record RefreshTokenCommand(string RefreshToken) : IRequest<Result<AuthResponse>>;
public sealed record RefreshTokenCommand(string RefreshToken) : IRequest<ErrorOr<AuthResponse>>;
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
using ErrorOr;
using MediatR;
using Voltiq.Application.Common.Interfaces;
using Voltiq.Domain.Common;
using Voltiq.Domain.Entities;
using Voltiq.Domain.Interfaces;
using Voltiq.Domain.Interfaces.Repositories;
using Voltiq.Domain.Interfaces.Repositories.User;
using Voltiq.Exceptions.Errors;
using Voltiq.Exceptions.Resources;

namespace Voltiq.Application.Features.Auth.Commands.Refresh;
Expand All @@ -15,29 +14,25 @@ public sealed class RefreshTokenCommandHandler(
IUserRepository userRepository,
ITokenService tokenService,
IUnitOfWork unitOfWork)
: IRequestHandler<RefreshTokenCommand, Result<AuthResponse>>
: IRequestHandler<RefreshTokenCommand, ErrorOr<AuthResponse>>
{
public async Task<Result<AuthResponse>> Handle(RefreshTokenCommand request, CancellationToken cancellationToken)
public async Task<ErrorOr<AuthResponse>> Handle(RefreshTokenCommand request, CancellationToken cancellationToken)
{
var refreshToken = await refreshTokenRepository.GetByTokenAsync(request.RefreshToken, cancellationToken);

if (refreshToken is null)
return Result<AuthResponse>.Failure(
new UnauthorizedError(ResourceErrorMessages.REFRESH_TOKEN_NAO_ENCONTRADO));
return Error.Unauthorized(description: ResourceErrorMessages.REFRESH_TOKEN_NAO_ENCONTRADO);

if (refreshToken.IsExpired)
return Result<AuthResponse>.Failure(
new UnauthorizedError(ResourceErrorMessages.REFRESH_TOKEN_EXPIRADO));
return Error.Unauthorized(description: ResourceErrorMessages.REFRESH_TOKEN_EXPIRADO);

if (!refreshToken.IsActive)
return Result<AuthResponse>.Failure(
new UnauthorizedError(ResourceErrorMessages.REFRESH_TOKEN_INVALIDO));
return Error.Unauthorized(description: ResourceErrorMessages.REFRESH_TOKEN_INVALIDO);

var user = await userRepository.GetByIdAsync(refreshToken.UserId, cancellationToken);

if (user is null)
return Result<AuthResponse>.Failure(
new UnauthorizedError(ResourceErrorMessages.REFRESH_TOKEN_INVALIDO));
return Error.Unauthorized(description: ResourceErrorMessages.REFRESH_TOKEN_INVALIDO);

refreshToken.Revoke();

Expand All @@ -48,6 +43,6 @@ public async Task<Result<AuthResponse>> Handle(RefreshTokenCommand request, Canc
await refreshTokenRepository.AddAsync(newRefreshToken, cancellationToken);
await unitOfWork.SaveChangesAsync(cancellationToken);

return Result<AuthResponse>.Success(new AuthResponse(newAccessToken, newRawRefreshToken));
return new AuthResponse(newAccessToken, newRawRefreshToken);
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
using ErrorOr;
using MediatR;
using Voltiq.Domain.Common;

namespace Voltiq.Application.Features.Users.Commands.CreateUser;

public sealed record CreateUserCommand(
string Name,
string Email,
string Document,
string Password) : IRequest<Result<CreateUserResponse>>;
string Password) : IRequest<ErrorOr<CreateUserResponse>>;
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
using ErrorOr;
using MediatR;
using Voltiq.Application.Common.Interfaces;
using Voltiq.Application.Mappings.Users;
using Voltiq.Domain.Common;
using Voltiq.Domain.Entities;
using Voltiq.Domain.Interfaces;
using Voltiq.Domain.Interfaces.Repositories.User;
using Voltiq.Domain.ValueObjects;
using Voltiq.Exceptions.Errors;
using Voltiq.Exceptions.Resources;

namespace Voltiq.Application.Features.Users.Commands.CreateUser;
Expand All @@ -16,9 +15,9 @@ public sealed class CreateUserCommandHandler(
IUnitOfWork unitOfWork,
IPasswordHasher passwordHasher,
ITokenService tokenService)
: IRequestHandler<CreateUserCommand, Result<CreateUserResponse>>
: IRequestHandler<CreateUserCommand, ErrorOr<CreateUserResponse>>
{
public async Task<Result<CreateUserResponse>> Handle(CreateUserCommand request, CancellationToken cancellationToken)
public async Task<ErrorOr<CreateUserResponse>> Handle(CreateUserCommand request, CancellationToken cancellationToken)
{
var email = Email.Create(request.Email).Value;
var document = Document.Create(request.Document).Value;
Expand All @@ -27,7 +26,7 @@ public async Task<Result<CreateUserResponse>> Handle(CreateUserCommand request,
document, email, cancellationToken);

if (userAlreadyExists)
return Result<CreateUserResponse>.Failure(new ConflictError(ResourceErrorMessages.USUARIO_EMAIL_JA_CADASTRADO));
return Error.Conflict(description: ResourceErrorMessages.USUARIO_EMAIL_JA_CADASTRADO);

var passwordHash = passwordHasher.Hash(request.Password);

Expand All @@ -38,6 +37,6 @@ public async Task<Result<CreateUserResponse>> Handle(CreateUserCommand request,

var token = tokenService.GenerateAccessToken(user.Id.ToString(), user.Name, []);

return Result<CreateUserResponse>.Success(user.ToCreateUserResponse(token));
return user.ToCreateUserResponse(token);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using ErrorOr;
using MediatR;
using Voltiq.Domain.Common;

namespace Voltiq.Application.Features.Users.Queries.GetCurrentUser;

public sealed record GetCurrentUserQuery : IRequest<Result<GetUserResponse>>;
public sealed record GetCurrentUserQuery : IRequest<ErrorOr<GetUserResponse>>;
Original file line number Diff line number Diff line change
@@ -1,31 +1,30 @@
using ErrorOr;
using MediatR;
using Voltiq.Application.Common.Interfaces;
using Voltiq.Application.Mappings.Users;
using Voltiq.Domain.Common;
using Voltiq.Domain.Entities;
using Voltiq.Domain.Interfaces.Repositories;
using Voltiq.Exceptions.Errors;
using Voltiq.Exceptions.Resources;

namespace Voltiq.Application.Features.Users.Queries.GetCurrentUser;

public sealed class GetCurrentUserQueryHandler(
ICurrentUserService currentUserService,
IRepository<User> userRepository)
: IRequestHandler<GetCurrentUserQuery, Result<GetUserResponse>>
: IRequestHandler<GetCurrentUserQuery, ErrorOr<GetUserResponse>>
{
public async Task<Result<GetUserResponse>> Handle(GetCurrentUserQuery request, CancellationToken cancellationToken)
public async Task<ErrorOr<GetUserResponse>> Handle(GetCurrentUserQuery request, CancellationToken cancellationToken)
{
if (!Guid.TryParse(currentUserService.UserId, out var userId) || userId == Guid.Empty)
return Result<GetUserResponse>.Failure(
new NotFoundError(string.Format(ResourceErrorMessages.ENTIDADE_NAO_ENCONTRADA, nameof(User), currentUserService.UserId)));
return Error.NotFound(description: string.Format(
ResourceErrorMessages.ENTIDADE_NAO_ENCONTRADA, nameof(User), currentUserService.UserId));

var user = await userRepository.GetByIdAsync(userId, cancellationToken);

if (user is null)
return Result<GetUserResponse>.Failure(
new NotFoundError(string.Format(ResourceErrorMessages.ENTIDADE_NAO_ENCONTRADA, nameof(User), userId)));
return Error.NotFound(description: string.Format(
ResourceErrorMessages.ENTIDADE_NAO_ENCONTRADA, nameof(User), userId));

return Result<GetUserResponse>.Success(user.ToGetUserResponse());
return user.ToGetUserResponse();
}
}
1 change: 1 addition & 0 deletions src/Voltiq.Application/Voltiq.Application.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="ErrorOr" Version="2.0.1" />
<PackageReference Include="FluentValidation" Version="12.1.1" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.1.1" />
<PackageReference Include="MediatR" Version="14.0.0" />
Expand Down
Loading
Loading