From 22986bb9e70d177090738f392901e903704cc891 Mon Sep 17 00:00:00 2001 From: "Jefferson L. da Silva" Date: Sat, 7 Feb 2026 19:24:07 -0300 Subject: [PATCH 1/3] Enables CORS and updates package versions Improves API security by adding the `Unauthorized` status code to endpoint definitions and enables CORS to allow requests from the frontend application running on `http://localhost:4200`. Updates package versions to their latest stable releases, including `Google.Apis.Gmail.v1`, `Microsoft.VisualStudio.Azure.Containers.Tools.Targets`, `Scalar.AspNetCore`, `SonarAnalyzer.CSharp`, and `MSTest` packages. The `CreateUser` endpoint is now accessible without authentication. Adds functionality to update basic user information. Adds functionality to change user password. --- Directory.Packages.props | 14 ++--- .../Endpoints/GoogleOAuthEndpoints.cs | 2 + .../Endpoints/InvoiceEndpoints.cs | 6 +++ .../Endpoints/JobScheduleEndpoints.cs | 10 +++- .../Endpoints/LoginEndpoints.cs | 8 ++- .../Endpoints/ScanEmailDefinitionEndpoints.cs | 7 +++ .../Endpoints/SendMessageEndpoints.cs | 1 + .../Endpoints/UserEndpoints.cs | 30 ++++++++++- .../Endpoints/UserPasswordEndpoints.cs | 25 +++++++++ InvoiceReminder.API/Program.cs | 9 ++++ .../AppServices/UserAppService.cs | 15 ++++++ .../AppServices/UserPasswordAppService.cs | 36 ++++++++++++- .../Interfaces/IUserAppService.cs | 3 +- .../Interfaces/IUserPasswordAppService.cs | 1 + .../InvoiceReminder.ArchitectureTests.csproj | 2 +- .../Abstractions/UserClaims.cs | 2 + .../Jwt/JwtObject.cs | 4 ++ .../Jwt/JwtProvider.cs | 4 ++ .../Interfaces/IUserPasswordRepository.cs | 1 + .../Interfaces/IUserRepository.cs | 1 + .../Repository/UserPasswordRepository.cs | 52 +++++++++++++++++++ .../Repository/UserRepository.cs | 47 +++++++++++++++++ .../InvoiceReminder.IntegrationTests.csproj | 2 +- .../Endpoints/UserEndpointsTests.cs | 31 +---------- .../InvoiceReminder.UnitTests.API.csproj | 2 +- ...voiceReminder.UnitTests.Application.csproj | 2 +- .../InvoiceReminder.UnitTests.Domain.csproj | 2 +- ...Reminder.UnitTests.ExternalServices.csproj | 2 +- .../Authentication/JwtProviderTests.cs | 4 +- ...ceReminder.UnitTests.Infrastructure.csproj | 2 +- ...oiceReminder.UnitTests.JobScheduler.csproj | 2 +- 31 files changed, 277 insertions(+), 52 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 98a3848..822d193 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -31,7 +31,7 @@ - + @@ -39,13 +39,13 @@ - + - + - + @@ -63,8 +63,8 @@ - - - + + + diff --git a/InvoiceReminder.API/Endpoints/GoogleOAuthEndpoints.cs b/InvoiceReminder.API/Endpoints/GoogleOAuthEndpoints.cs index ec4cc91..d94a412 100644 --- a/InvoiceReminder.API/Endpoints/GoogleOAuthEndpoints.cs +++ b/InvoiceReminder.API/Endpoints/GoogleOAuthEndpoints.cs @@ -29,6 +29,7 @@ private static void MapGetAuthUrl(RouteGroupBuilder endpoint) .RequireAuthorization() .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status500InternalServerError); } @@ -64,6 +65,7 @@ private static void MapRevoke(RouteGroupBuilder endpoint) .RequireAuthorization() .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status500InternalServerError); } } diff --git a/InvoiceReminder.API/Endpoints/InvoiceEndpoints.cs b/InvoiceReminder.API/Endpoints/InvoiceEndpoints.cs index 58c31d5..f4b8a94 100644 --- a/InvoiceReminder.API/Endpoints/InvoiceEndpoints.cs +++ b/InvoiceReminder.API/Endpoints/InvoiceEndpoints.cs @@ -34,6 +34,7 @@ private static void MapGetInvoices(RouteGroupBuilder endpoint) .RequireAuthorization() .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status500InternalServerError); } @@ -51,6 +52,7 @@ private static void MapGetInvoice(RouteGroupBuilder endpoint) .RequireAuthorization() .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status500InternalServerError); } @@ -70,6 +72,7 @@ private static void MapGetInvoiceByBarcode(RouteGroupBuilder endpoint) .RequireAuthorization() .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status500InternalServerError); } @@ -89,6 +92,7 @@ private static void MapCreateInvoice(RouteGroupBuilder endpoint) .RequireAuthorization() .Produces(StatusCodes.Status201Created) .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status500InternalServerError); } @@ -107,6 +111,7 @@ private static void MapUpdateInvoice(RouteGroupBuilder endpoint) .RequireAuthorization() .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status500InternalServerError); } @@ -125,6 +130,7 @@ private static void MapDeleteInvoice(RouteGroupBuilder endpoint) .RequireAuthorization() .Produces(StatusCodes.Status204NoContent) .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status500InternalServerError); } } diff --git a/InvoiceReminder.API/Endpoints/JobScheduleEndpoints.cs b/InvoiceReminder.API/Endpoints/JobScheduleEndpoints.cs index ed9e853..10e9199 100644 --- a/InvoiceReminder.API/Endpoints/JobScheduleEndpoints.cs +++ b/InvoiceReminder.API/Endpoints/JobScheduleEndpoints.cs @@ -34,6 +34,7 @@ private static void MapGetJobSchedules(RouteGroupBuilder endpoint) .RequireAuthorization() .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status500InternalServerError); } @@ -51,8 +52,9 @@ private static void MapGetJobSchedule(RouteGroupBuilder endpoint) .WithName("GetJobSchedule") .RequireAuthorization() .Produces(StatusCodes.Status200OK) - .Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status500InternalServerError); } @@ -70,8 +72,9 @@ private static void MapGetJobScheduleByUserId(RouteGroupBuilder endpoint) .WithName("GetJobScheduleByUserId") .RequireAuthorization() .Produces(StatusCodes.Status200OK) - .Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status500InternalServerError); } @@ -91,6 +94,7 @@ private static void MapCreateJobSchedule(RouteGroupBuilder endpoint) .RequireAuthorization() .Produces(StatusCodes.Status201Created) .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status500InternalServerError); } @@ -110,6 +114,7 @@ private static void MapUpdateJobSchedule(RouteGroupBuilder endpoint) .RequireAuthorization() .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status500InternalServerError); } @@ -129,6 +134,7 @@ private static void MapDeleteJobSchedule(RouteGroupBuilder endpoint) .RequireAuthorization() .Produces(StatusCodes.Status204NoContent) .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status500InternalServerError); } } diff --git a/InvoiceReminder.API/Endpoints/LoginEndpoints.cs b/InvoiceReminder.API/Endpoints/LoginEndpoints.cs index 9f085ea..43d3254 100644 --- a/InvoiceReminder.API/Endpoints/LoginEndpoints.cs +++ b/InvoiceReminder.API/Endpoints/LoginEndpoints.cs @@ -31,7 +31,13 @@ private static void MapLogin(IEndpointRouteBuilder endpoints) var result = await appService.ValidateUserPasswordAsync(request.Email, request.Password, ct); return result.IsSuccess - ? Results.Ok(jwtProvider.Generate(new UserClaims { Id = result.Value.Id, Email = result.Value.Email })) + ? Results.Ok(jwtProvider.Generate(new UserClaims + { + Id = result.Value.Id, + Name = result.Value.Name, + Email = result.Value.Email, + TelegramChatId = result.Value.TelegramChatId + })) : Results.Unauthorized(); }) .WithName("Login") diff --git a/InvoiceReminder.API/Endpoints/ScanEmailDefinitionEndpoints.cs b/InvoiceReminder.API/Endpoints/ScanEmailDefinitionEndpoints.cs index 6986288..bd65d6c 100644 --- a/InvoiceReminder.API/Endpoints/ScanEmailDefinitionEndpoints.cs +++ b/InvoiceReminder.API/Endpoints/ScanEmailDefinitionEndpoints.cs @@ -35,6 +35,7 @@ private static void MapGetScanEmailDefinitions(RouteGroupBuilder endpoint) .RequireAuthorization() .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status500InternalServerError); } @@ -53,6 +54,7 @@ private static void MapGetScanEmailDefinition(RouteGroupBuilder endpoint) .RequireAuthorization() .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status500InternalServerError); } @@ -72,6 +74,7 @@ private static void MapGetByUserId(RouteGroupBuilder endpoint) .RequireAuthorization() .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status500InternalServerError); } @@ -91,6 +94,7 @@ private static void MapGetBySenderEmailAddress(RouteGroupBuilder endpoint) .RequireAuthorization() .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status500InternalServerError); } @@ -111,6 +115,7 @@ private static void MapCreateScanEmailDefinition(RouteGroupBuilder endpoint) .RequireAuthorization() .Produces(StatusCodes.Status201Created) .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status500InternalServerError); } @@ -130,6 +135,7 @@ private static void MapUpdateScanEmailDefinition(RouteGroupBuilder endpoint) .RequireAuthorization() .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status500InternalServerError); } @@ -149,6 +155,7 @@ private static void MapDeleteScanEmailDefinition(RouteGroupBuilder endpoint) .RequireAuthorization() .Produces(StatusCodes.Status204NoContent) .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status500InternalServerError); } } diff --git a/InvoiceReminder.API/Endpoints/SendMessageEndpoints.cs b/InvoiceReminder.API/Endpoints/SendMessageEndpoints.cs index a52811d..2f84fdd 100644 --- a/InvoiceReminder.API/Endpoints/SendMessageEndpoints.cs +++ b/InvoiceReminder.API/Endpoints/SendMessageEndpoints.cs @@ -30,6 +30,7 @@ private static void MapSendMessage(RouteGroupBuilder endpoint) .WithName("SendMessage") .RequireAuthorization() .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status400BadRequest) .Produces(StatusCodes.Status500InternalServerError); } diff --git a/InvoiceReminder.API/Endpoints/UserEndpoints.cs b/InvoiceReminder.API/Endpoints/UserEndpoints.cs index fd6b971..3b0985d 100644 --- a/InvoiceReminder.API/Endpoints/UserEndpoints.cs +++ b/InvoiceReminder.API/Endpoints/UserEndpoints.cs @@ -18,6 +18,7 @@ public void RegisterEndpoints(IEndpointRouteBuilder endpoints) MapCreateUser(endpoint); MapCreateUsers(endpoint); MapUpdateUser(endpoint); + MapUpdateBasicUserInfo(endpoint); MapDeleteUser(endpoint); } @@ -35,6 +36,7 @@ private static void MapGetUsers(RouteGroupBuilder endpoint) .RequireAuthorization() .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status500InternalServerError); } @@ -51,6 +53,8 @@ private static void MapGetUser(RouteGroupBuilder endpoint) .WithName("GetUser") .RequireAuthorization() .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status500InternalServerError); } @@ -69,6 +73,8 @@ private static void MapGetUserByEmail(RouteGroupBuilder endpoint) .WithName("GetUserByEmail") .RequireAuthorization() .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status500InternalServerError); } @@ -85,7 +91,7 @@ private static void MapCreateUser(RouteGroupBuilder endpoint) : Results.Problem(result.Error); }) .WithName("CreateUser") - .RequireAuthorization() + .AllowAnonymous() .Produces(StatusCodes.Status201Created) .Produces(StatusCodes.Status400BadRequest) .Produces(StatusCodes.Status500InternalServerError); @@ -107,6 +113,7 @@ private static void MapCreateUsers(RouteGroupBuilder endpoint) .RequireAuthorization() .Produces(StatusCodes.Status201Created) .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status500InternalServerError); } @@ -125,6 +132,26 @@ private static void MapUpdateUser(RouteGroupBuilder endpoint) .RequireAuthorization() .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status500InternalServerError); + } + + private static void MapUpdateBasicUserInfo(RouteGroupBuilder endpoint) + { + _ = endpoint.MapPatch("/", + async (IUserAppService appService, [FromBody] UserViewModel viewModel, CancellationToken ct) => + { + var result = await appService.UpdateBasicUserInfoAsync(viewModel, ct); + + return result.IsSuccess + ? Results.Ok(result.Value) + : Results.Problem(result.Error); + }) + .WithName("UpdateBasicUserInfo") + .RequireAuthorization() + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status500InternalServerError); } @@ -143,6 +170,7 @@ private static void MapDeleteUser(RouteGroupBuilder endpoint) .RequireAuthorization() .Produces(StatusCodes.Status204NoContent) .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status500InternalServerError); } } diff --git a/InvoiceReminder.API/Endpoints/UserPasswordEndpoints.cs b/InvoiceReminder.API/Endpoints/UserPasswordEndpoints.cs index 997330e..fbe8c57 100644 --- a/InvoiceReminder.API/Endpoints/UserPasswordEndpoints.cs +++ b/InvoiceReminder.API/Endpoints/UserPasswordEndpoints.cs @@ -14,6 +14,7 @@ public void RegisterEndpoints(IEndpointRouteBuilder endpoints) MapCreateUserPassword(endpoint); MapCreateUsersPassword(endpoint); + MapChangeUserPassword(endpoint); MapDeleteUserPassword(endpoint); MapUpdateUserPassword(endpoint); } @@ -33,6 +34,7 @@ private static void MapCreateUserPassword(RouteGroupBuilder endpoint) .RequireAuthorization() .Produces(StatusCodes.Status201Created) .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status500InternalServerError); } @@ -52,6 +54,27 @@ private static void MapCreateUsersPassword(RouteGroupBuilder endpoint) .RequireAuthorization() .Produces(StatusCodes.Status201Created) .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status500InternalServerError); + } + + private static void MapChangeUserPassword(RouteGroupBuilder endpoint) + { + _ = endpoint.MapPatch("/", + async (IUserPasswordAppService appService, [FromBody] UserPasswordViewModel viewModel, + CancellationToken ct) => + { + var result = await appService.ChangePasswordAsync(viewModel, ct); + + return result.IsSuccess + ? Results.Ok(result.Value) + : Results.Problem(result.Error); + }) + .WithName("ChangeUserPassword") + .RequireAuthorization() + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status500InternalServerError); } @@ -71,6 +94,7 @@ private static void MapUpdateUserPassword(RouteGroupBuilder endpoint) .RequireAuthorization() .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status500InternalServerError); } @@ -90,6 +114,7 @@ private static void MapDeleteUserPassword(RouteGroupBuilder endpoint) .RequireAuthorization() .Produces(StatusCodes.Status204NoContent) .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status500InternalServerError); } } diff --git a/InvoiceReminder.API/Program.cs b/InvoiceReminder.API/Program.cs index 40fc1dd..c6a8180 100644 --- a/InvoiceReminder.API/Program.cs +++ b/InvoiceReminder.API/Program.cs @@ -11,6 +11,14 @@ builder.Services.AddExceptionHandler(); builder.Services.AddInfrastructure(); builder.Services.AddOpenApi(opt => opt.AddDocumentTransformer()); +builder.Services.AddCors(opt => + opt.AddPolicy("CorsPolicy", builder => + builder.WithOrigins("http://localhost:4200") + .AllowAnyMethod() + .AllowCredentials() + .WithHeaders("Content-Type", "Authorization") + ) +); builder.Services.AddHealthChecks().AddNpgSql ( connectionString: builder.Configuration.GetConnectionString("DataBaseConnection"), @@ -32,6 +40,7 @@ _ = app.UseHsts(); } +app.UseCors("CorsPolicy"); app.RegisterEndpoints(); app.UseExceptionHandler(); app.UseStatusCodePages(); diff --git a/InvoiceReminder.Application/AppServices/UserAppService.cs b/InvoiceReminder.Application/AppServices/UserAppService.cs index c42829e..70df77f 100644 --- a/InvoiceReminder.Application/AppServices/UserAppService.cs +++ b/InvoiceReminder.Application/AppServices/UserAppService.cs @@ -74,4 +74,19 @@ public async Task> ValidateUserPasswordAsync( ? Result.Failure("User not Found.") : Result.Success(entity.Adapt()); } + + public async Task> UpdateBasicUserInfoAsync( + UserViewModel viewModel, + CancellationToken cancellationToken = default) + { + if (viewModel is null) + { + return Result.Failure($"Parameter {nameof(viewModel)} was Null."); + } + + var entity = viewModel.Adapt(); + var result = await _repository.UpdateBasicUserInfoAsync(entity, cancellationToken); + + return Result.Success(result); + } } diff --git a/InvoiceReminder.Application/AppServices/UserPasswordAppService.cs b/InvoiceReminder.Application/AppServices/UserPasswordAppService.cs index 337bd2d..dcd45f5 100644 --- a/InvoiceReminder.Application/AppServices/UserPasswordAppService.cs +++ b/InvoiceReminder.Application/AppServices/UserPasswordAppService.cs @@ -15,8 +15,10 @@ public class UserPasswordAppService : BaseAppService("Security:ParallelismFactor"); _repository = repository; @@ -78,6 +80,36 @@ public override async Task> BulkInsertAsync( return Result.Success(result); } + public async Task> ChangePasswordAsync( + UserPasswordViewModel viewModel, + CancellationToken cancellationToken = default) + { + if (viewModel is null) + { + return Result.Failure("The provided object data was Null."); + } + + if (string.IsNullOrWhiteSpace(viewModel.PasswordHash)) + { + return Result.Failure("Password is required."); + } + + (var pHash, var pSalt) = viewModel.PasswordHash.HashPassword(_parallelismFactor); + viewModel.PasswordHash = pHash; + viewModel.PasswordSalt = pSalt; + + var result = await _repository.ChangePasswordAsync(viewModel.Adapt(), cancellationToken); + + if (!result) + { + return Result.Failure("Failed to change the password."); + } + + await _unitOfWork.SaveChangesAsync(cancellationToken); + + return Result.Success(result); + } + public async Task> GetByUserIdAsync( Guid userId, CancellationToken cancellationToken = default) diff --git a/InvoiceReminder.Application/Interfaces/IUserAppService.cs b/InvoiceReminder.Application/Interfaces/IUserAppService.cs index 7c2fc1a..f22a857 100644 --- a/InvoiceReminder.Application/Interfaces/IUserAppService.cs +++ b/InvoiceReminder.Application/Interfaces/IUserAppService.cs @@ -7,6 +7,7 @@ namespace InvoiceReminder.Application.Interfaces; public interface IUserAppService : IBaseAppService { Task> GetByEmailAsync(string value, CancellationToken cancellationToken = default); + Task> UpdateBasicUserInfoAsync(UserViewModel viewModel, CancellationToken cancellationToken = default); Task> ValidateUserPasswordAsync(string email, string password, - CancellationToken cancellationToken = default); + CancellationToken cancellationToken = default); } diff --git a/InvoiceReminder.Application/Interfaces/IUserPasswordAppService.cs b/InvoiceReminder.Application/Interfaces/IUserPasswordAppService.cs index 1823030..67e486d 100644 --- a/InvoiceReminder.Application/Interfaces/IUserPasswordAppService.cs +++ b/InvoiceReminder.Application/Interfaces/IUserPasswordAppService.cs @@ -7,4 +7,5 @@ namespace InvoiceReminder.Application.Interfaces; public interface IUserPasswordAppService : IBaseAppService { Task> GetByUserIdAsync(Guid userId, CancellationToken cancellationToken = default); + Task> ChangePasswordAsync(UserPasswordViewModel viewModel, CancellationToken cancellationToken = default); } diff --git a/InvoiceReminder.ArchitectureTests/InvoiceReminder.ArchitectureTests.csproj b/InvoiceReminder.ArchitectureTests/InvoiceReminder.ArchitectureTests.csproj index 70853db..803b79a 100644 --- a/InvoiceReminder.ArchitectureTests/InvoiceReminder.ArchitectureTests.csproj +++ b/InvoiceReminder.ArchitectureTests/InvoiceReminder.ArchitectureTests.csproj @@ -1,4 +1,4 @@ - + true diff --git a/InvoiceReminder.Authentication/Abstractions/UserClaims.cs b/InvoiceReminder.Authentication/Abstractions/UserClaims.cs index 7824f0f..0595e4b 100644 --- a/InvoiceReminder.Authentication/Abstractions/UserClaims.cs +++ b/InvoiceReminder.Authentication/Abstractions/UserClaims.cs @@ -3,5 +3,7 @@ namespace InvoiceReminder.Authentication.Abstractions; public record UserClaims { public required Guid Id { get; init; } + public required string Name { get; init; } public required string Email { get; init; } + public required long TelegramChatId { get; set; } } diff --git a/InvoiceReminder.Authentication/Jwt/JwtObject.cs b/InvoiceReminder.Authentication/Jwt/JwtObject.cs index 2e867d2..a83f29d 100644 --- a/InvoiceReminder.Authentication/Jwt/JwtObject.cs +++ b/InvoiceReminder.Authentication/Jwt/JwtObject.cs @@ -2,7 +2,11 @@ namespace InvoiceReminder.Authentication.Jwt; public class JwtObject { + public Guid UserId { get; set; } + public string Name { get; set; } + public string Email { get; set; } public string AuthenticationToken { get; set; } public bool Authenticated { get; set; } + public long TelegramChatId { get; set; } public DateTime Expiration { get; set; } } diff --git a/InvoiceReminder.Authentication/Jwt/JwtProvider.cs b/InvoiceReminder.Authentication/Jwt/JwtProvider.cs index 8c57650..3114d52 100644 --- a/InvoiceReminder.Authentication/Jwt/JwtProvider.cs +++ b/InvoiceReminder.Authentication/Jwt/JwtProvider.cs @@ -56,8 +56,12 @@ public JwtObject Generate(UserClaims user) return new JwtObject { + UserId = user.Id, + Name = user.Name, + Email = user.Email, AuthenticationToken = tokenHandler.WriteToken(token), Authenticated = true, + TelegramChatId = user.TelegramChatId, Expiration = token.ValidTo }; } diff --git a/InvoiceReminder.Data/Interfaces/IUserPasswordRepository.cs b/InvoiceReminder.Data/Interfaces/IUserPasswordRepository.cs index e990407..1b4041f 100644 --- a/InvoiceReminder.Data/Interfaces/IUserPasswordRepository.cs +++ b/InvoiceReminder.Data/Interfaces/IUserPasswordRepository.cs @@ -5,4 +5,5 @@ namespace InvoiceReminder.Data.Interfaces; public interface IUserPasswordRepository : IBaseRepository { Task GetByUserIdAsync(Guid userId, CancellationToken cancellationToken = default); + Task ChangePasswordAsync(UserPassword userPassword, CancellationToken cancellationToken = default); } diff --git a/InvoiceReminder.Data/Interfaces/IUserRepository.cs b/InvoiceReminder.Data/Interfaces/IUserRepository.cs index c381d5c..86c3352 100644 --- a/InvoiceReminder.Data/Interfaces/IUserRepository.cs +++ b/InvoiceReminder.Data/Interfaces/IUserRepository.cs @@ -5,4 +5,5 @@ namespace InvoiceReminder.Data.Interfaces; public interface IUserRepository : IBaseRepository { Task GetByEmailAsync(string value, CancellationToken cancellationToken = default); + Task UpdateBasicUserInfoAsync(User user, CancellationToken cancellationToken = default); } diff --git a/InvoiceReminder.Data/Repository/UserPasswordRepository.cs b/InvoiceReminder.Data/Repository/UserPasswordRepository.cs index 9828bd5..fcb68f6 100644 --- a/InvoiceReminder.Data/Repository/UserPasswordRepository.cs +++ b/InvoiceReminder.Data/Repository/UserPasswordRepository.cs @@ -21,6 +21,58 @@ public UserPasswordRepository(CoreDbContext dbContext, ILogger ChangePasswordAsync(UserPassword userPassword, CancellationToken cancellationToken = default) + { + var result = false; + var query = """ + update invoice_reminder.user_password + set + password_hash = @passwordHash, + password_salt = @passwordSalt, + updated_at = now() + where user_id = @userId + """; + + try + { + var parameters = new DynamicParameters(new + { + passwordHash = userPassword.PasswordHash, + passwordSalt = userPassword.PasswordSalt, + userId = userPassword.UserId + }); + var command = new CommandDefinition(query, parameters, cancellationToken: cancellationToken); + + result = await _dbConnection.ExecuteAsync(command) > 0; + } + catch (OperationCanceledException ex) when (cancellationToken.IsCancellationRequested) + { + var method = $"{nameof(UserPasswordRepository)}.{nameof(ChangePasswordAsync)}"; + var contextualInfo = $"Method {method} execution was interrupted by a CancellationToken Request..."; + + if (_logger.IsEnabled(LogLevel.Warning)) + { + _logger.LogWarning(ex, LogExceptionMessage, contextualInfo, ex.Message); + } + + throw new OperationCanceledException(contextualInfo, ex, cancellationToken); + } + catch (Exception ex) + { + var method = $"{nameof(UserPasswordRepository)}.{nameof(ChangePasswordAsync)}"; + var contextualInfo = $"Exception raised while updating DB >> {method}(...)"; + + if (_logger.IsEnabled(LogLevel.Error)) + { + _logger.LogError(ex, LogExceptionMessage, contextualInfo, ex.Message); + } + + throw new DataLayerException(contextualInfo, ex); + } + + return result; + } + public async Task GetByUserIdAsync(Guid userId, CancellationToken cancellationToken = default) { UserPassword userPassword = default; diff --git a/InvoiceReminder.Data/Repository/UserRepository.cs b/InvoiceReminder.Data/Repository/UserRepository.cs index 0c2b93e..3c822f1 100644 --- a/InvoiceReminder.Data/Repository/UserRepository.cs +++ b/InvoiceReminder.Data/Repository/UserRepository.cs @@ -145,4 +145,51 @@ public override async Task GetByIdAsync(Guid id, CancellationToken cancell return result.FirstOrDefault().Value; } + + public async Task UpdateBasicUserInfoAsync(User user, CancellationToken cancellationToken = default) + { + var result = false; + var query = """ + update invoice_reminder.user + set name = @Name, + email = @Email, + telegram_chat_id = @TelegramChatId, + updated_at = now() + where id = @Id + """; + + try + { + var parameters = new DynamicParameters(new { user.Name, user.Email, user.TelegramChatId, user.Id }); + var command = new CommandDefinition(query, parameters, cancellationToken: cancellationToken); + + result = await _dbConnection.ExecuteAsync(command) > 0; + } + catch (OperationCanceledException ex) when (cancellationToken.IsCancellationRequested) + { + var method = $"{nameof(UserRepository)}.{nameof(UpdateBasicUserInfoAsync)}"; + var contextualInfo = $"Method {method} execution was interrupted by a CancellationToken Request..."; + + if (_logger.IsEnabled(LogLevel.Warning)) + { + _logger.LogWarning(ex, LogExceptionMessage, contextualInfo, ex.Message); + } + + throw new OperationCanceledException(contextualInfo, ex, cancellationToken); + } + catch (Exception ex) + { + var method = $"{nameof(UserRepository)}.{nameof(UpdateBasicUserInfoAsync)}"; + var contextualInfo = $"Exception raised while updating Entity >> {method}(...)"; + + if (_logger.IsEnabled(LogLevel.Error)) + { + _logger.LogError(ex, LogExceptionMessage, contextualInfo, ex.Message); + } + + throw new DataLayerException(contextualInfo, ex); + } + + return result; + } } diff --git a/InvoiceReminder.IntegrationTests/InvoiceReminder.IntegrationTests.csproj b/InvoiceReminder.IntegrationTests/InvoiceReminder.IntegrationTests.csproj index 4e07c9e..9e79e2e 100644 --- a/InvoiceReminder.IntegrationTests/InvoiceReminder.IntegrationTests.csproj +++ b/InvoiceReminder.IntegrationTests/InvoiceReminder.IntegrationTests.csproj @@ -1,4 +1,4 @@ - + CA1873 diff --git a/InvoiceReminder.UnitTests.API/Endpoints/UserEndpointsTests.cs b/InvoiceReminder.UnitTests.API/Endpoints/UserEndpointsTests.cs index 93e2a3c..1a85a20 100644 --- a/InvoiceReminder.UnitTests.API/Endpoints/UserEndpointsTests.cs +++ b/InvoiceReminder.UnitTests.API/Endpoints/UserEndpointsTests.cs @@ -385,11 +385,10 @@ public async Task GetUserByEmail_WhenUserIsAuthenticatedButNotExists_ShouldRetur #region MapPost Tests [TestMethod] - public async Task CreateUser_WhenUserIsAuthenticated_ShouldReturnCreated() + public async Task CreateUser_ShouldReturnCreated() { // Arrange var request = new HttpRequestMessage(HttpMethod.Post, basepath); - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "test_token"); var userViewModel = _userViewModelFaker.Generate(); var expectedResult = Result.Success(userViewModel); @@ -397,10 +396,6 @@ public async Task CreateUser_WhenUserIsAuthenticated_ShouldReturnCreated() _ = _userAppService.AddAsync(Arg.Any(), Arg.Any()) .Returns(expectedResult); - _ = _authorizationService.AuthorizeAsync(Arg.Any(), Arg.Any(), - Arg.Any>()) - .Returns(Task.FromResult(AuthorizationResult.Success())); - // Act request.Content = JsonContent.Create(userViewModel); var response = await _client.SendAsync(request, TestContext.CancellationToken); @@ -415,28 +410,10 @@ public async Task CreateUser_WhenUserIsAuthenticated_ShouldReturnCreated() } [TestMethod] - public async Task CreateUser_WhenUserIsNotAuthenticated_ShouldReturnUnauthorized() - { - // Arrange - var request = new HttpRequestMessage(HttpMethod.Post, basepath); - - _ = _authorizationService.AuthorizeAsync(Arg.Any(), Arg.Any(), - Arg.Any>()) - .Returns(Task.FromResult(AuthorizationResult.Failed())); - - // Act - var response = await _client.SendAsync(request, TestContext.CancellationToken); - - // Assert - response.StatusCode.ShouldBe(HttpStatusCode.Unauthorized); - } - - [TestMethod] - public async Task CreateUser_WhenUserIsAuthenticatedButServiceFails_ShouldReturnInternalServerError() + public async Task CreateUser_WhenServiceFails_ShouldReturnInternalServerError() { // Arrange var request = new HttpRequestMessage(HttpMethod.Post, basepath); - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "test_token"); var userViewModel = _userViewModelFaker.Generate(); var expectedResult = Result.Failure("Service error"); @@ -444,10 +421,6 @@ public async Task CreateUser_WhenUserIsAuthenticatedButServiceFails_ShouldReturn _ = _userAppService.AddAsync(Arg.Any(), Arg.Any()) .Returns(expectedResult); - _ = _authorizationService.AuthorizeAsync(Arg.Any(), Arg.Any(), - Arg.Any>()) - .Returns(Task.FromResult(AuthorizationResult.Success())); - // Act request.Content = JsonContent.Create(userViewModel); var response = await _client.SendAsync(request, TestContext.CancellationToken); diff --git a/InvoiceReminder.UnitTests.API/InvoiceReminder.UnitTests.API.csproj b/InvoiceReminder.UnitTests.API/InvoiceReminder.UnitTests.API.csproj index d18b759..298ec50 100644 --- a/InvoiceReminder.UnitTests.API/InvoiceReminder.UnitTests.API.csproj +++ b/InvoiceReminder.UnitTests.API/InvoiceReminder.UnitTests.API.csproj @@ -1,4 +1,4 @@ - + true diff --git a/InvoiceReminder.UnitTests.Application/InvoiceReminder.UnitTests.Application.csproj b/InvoiceReminder.UnitTests.Application/InvoiceReminder.UnitTests.Application.csproj index 0a9af9a..199d191 100644 --- a/InvoiceReminder.UnitTests.Application/InvoiceReminder.UnitTests.Application.csproj +++ b/InvoiceReminder.UnitTests.Application/InvoiceReminder.UnitTests.Application.csproj @@ -1,4 +1,4 @@ - + true diff --git a/InvoiceReminder.UnitTests.Domain/InvoiceReminder.UnitTests.Domain.csproj b/InvoiceReminder.UnitTests.Domain/InvoiceReminder.UnitTests.Domain.csproj index f3effe2..f1eced2 100644 --- a/InvoiceReminder.UnitTests.Domain/InvoiceReminder.UnitTests.Domain.csproj +++ b/InvoiceReminder.UnitTests.Domain/InvoiceReminder.UnitTests.Domain.csproj @@ -1,4 +1,4 @@ - + true diff --git a/InvoiceReminder.UnitTests.ExternalServices/InvoiceReminder.UnitTests.ExternalServices.csproj b/InvoiceReminder.UnitTests.ExternalServices/InvoiceReminder.UnitTests.ExternalServices.csproj index 0e6d6fb..271ba4a 100644 --- a/InvoiceReminder.UnitTests.ExternalServices/InvoiceReminder.UnitTests.ExternalServices.csproj +++ b/InvoiceReminder.UnitTests.ExternalServices/InvoiceReminder.UnitTests.ExternalServices.csproj @@ -1,4 +1,4 @@ - + CA1873 diff --git a/InvoiceReminder.UnitTests.Infrastructure/Authentication/JwtProviderTests.cs b/InvoiceReminder.UnitTests.Infrastructure/Authentication/JwtProviderTests.cs index de68f6c..c1a0571 100644 --- a/InvoiceReminder.UnitTests.Infrastructure/Authentication/JwtProviderTests.cs +++ b/InvoiceReminder.UnitTests.Infrastructure/Authentication/JwtProviderTests.cs @@ -71,7 +71,9 @@ public void JwtProvider_Generate_ValidUser_ShouldReturnToken() var user = new UserClaims { Id = Guid.NewGuid(), - Email = "user@test.com" + Name = "Test User", + Email = "user@test.com", + TelegramChatId = 1234567890 }; // Act diff --git a/InvoiceReminder.UnitTests.Infrastructure/InvoiceReminder.UnitTests.Infrastructure.csproj b/InvoiceReminder.UnitTests.Infrastructure/InvoiceReminder.UnitTests.Infrastructure.csproj index f025a73..f7a5494 100644 --- a/InvoiceReminder.UnitTests.Infrastructure/InvoiceReminder.UnitTests.Infrastructure.csproj +++ b/InvoiceReminder.UnitTests.Infrastructure/InvoiceReminder.UnitTests.Infrastructure.csproj @@ -1,4 +1,4 @@ - + true diff --git a/InvoiceReminder.UnitTests.JobScheduler/InvoiceReminder.UnitTests.JobScheduler.csproj b/InvoiceReminder.UnitTests.JobScheduler/InvoiceReminder.UnitTests.JobScheduler.csproj index ecd0d7c..2a4b76c 100644 --- a/InvoiceReminder.UnitTests.JobScheduler/InvoiceReminder.UnitTests.JobScheduler.csproj +++ b/InvoiceReminder.UnitTests.JobScheduler/InvoiceReminder.UnitTests.JobScheduler.csproj @@ -1,4 +1,4 @@ - + CA1873 From 795a3a41194416eeb4037257401585ed706f0568 Mon Sep 17 00:00:00 2001 From: "Jefferson L. da Silva" Date: Sun, 8 Feb 2026 18:18:13 -0300 Subject: [PATCH 2/3] Enables CORS for frontend integration Configures CORS to allow specific origins from the configuration, enabling integration with the frontend application. This change ensures that the API can securely handle requests from the specified frontend origin. Also, refactors the `UserAppService` and `UserPasswordAppService` to handle update failures more gracefully, returning a failure result with a descriptive message. Finally, simplifies JwtObject and includes telegramChatId in jwt claims. --- InvoiceReminder.API/Program.cs | 17 ++++++++++++----- InvoiceReminder.API/appsettings.json | 5 +++++ .../AppServices/UserAppService.cs | 4 +++- .../AppServices/UserPasswordAppService.cs | 11 +++-------- InvoiceReminder.Authentication/Jwt/JwtObject.cs | 4 ---- .../Jwt/JwtProvider.cs | 8 +++----- .../Repository/UserRepository.cs | 12 ++++++++++++ 7 files changed, 38 insertions(+), 23 deletions(-) diff --git a/InvoiceReminder.API/Program.cs b/InvoiceReminder.API/Program.cs index c6a8180..2c3a427 100644 --- a/InvoiceReminder.API/Program.cs +++ b/InvoiceReminder.API/Program.cs @@ -11,14 +11,21 @@ builder.Services.AddExceptionHandler(); builder.Services.AddInfrastructure(); builder.Services.AddOpenApi(opt => opt.AddDocumentTransformer()); + builder.Services.AddCors(opt => - opt.AddPolicy("CorsPolicy", builder => - builder.WithOrigins("http://localhost:4200") - .AllowAnyMethod() - .AllowCredentials() - .WithHeaders("Content-Type", "Authorization") + opt.AddPolicy("CorsPolicy", policy => + { + var allowedOrigins = builder.Configuration.GetSection("Cors:AllowedOrigins").Get() ?? []; + + _ = policy + .AllowAnyMethod() + .AllowCredentials() + .WithOrigins(allowedOrigins) + .WithHeaders("Content-Type", "Authorization"); + } ) ); + builder.Services.AddHealthChecks().AddNpgSql ( connectionString: builder.Configuration.GetConnectionString("DataBaseConnection"), diff --git a/InvoiceReminder.API/appsettings.json b/InvoiceReminder.API/appsettings.json index 766550e..37ff50a 100644 --- a/InvoiceReminder.API/appsettings.json +++ b/InvoiceReminder.API/appsettings.json @@ -8,6 +8,11 @@ "AllowedHosts": "*", "Timeout": 120, "ProviderName": "Npgsql", + "Cors": { + "AllowedOrigins": [ + "SECRET_ORIGIN" + ] + }, "appKeys": { "googleOauthClientId": "SECRET_CLIENT_ID", "googleOauthClientSecret": "SECRET_CLIENT_SECRET", diff --git a/InvoiceReminder.Application/AppServices/UserAppService.cs b/InvoiceReminder.Application/AppServices/UserAppService.cs index 70df77f..c97d675 100644 --- a/InvoiceReminder.Application/AppServices/UserAppService.cs +++ b/InvoiceReminder.Application/AppServices/UserAppService.cs @@ -87,6 +87,8 @@ public async Task> UpdateBasicUserInfoAsync( var entity = viewModel.Adapt(); var result = await _repository.UpdateBasicUserInfoAsync(entity, cancellationToken); - return Result.Success(result); + return result + ? Result.Success(result) + : Result.Failure("User not Found!"); } } diff --git a/InvoiceReminder.Application/AppServices/UserPasswordAppService.cs b/InvoiceReminder.Application/AppServices/UserPasswordAppService.cs index dcd45f5..b152617 100644 --- a/InvoiceReminder.Application/AppServices/UserPasswordAppService.cs +++ b/InvoiceReminder.Application/AppServices/UserPasswordAppService.cs @@ -100,14 +100,9 @@ public async Task> ChangePasswordAsync( var result = await _repository.ChangePasswordAsync(viewModel.Adapt(), cancellationToken); - if (!result) - { - return Result.Failure("Failed to change the password."); - } - - await _unitOfWork.SaveChangesAsync(cancellationToken); - - return Result.Success(result); + return result + ? Result.Failure("Failed to change the password.") + : Result.Success(result); } public async Task> GetByUserIdAsync( diff --git a/InvoiceReminder.Authentication/Jwt/JwtObject.cs b/InvoiceReminder.Authentication/Jwt/JwtObject.cs index a83f29d..2e867d2 100644 --- a/InvoiceReminder.Authentication/Jwt/JwtObject.cs +++ b/InvoiceReminder.Authentication/Jwt/JwtObject.cs @@ -2,11 +2,7 @@ namespace InvoiceReminder.Authentication.Jwt; public class JwtObject { - public Guid UserId { get; set; } - public string Name { get; set; } - public string Email { get; set; } public string AuthenticationToken { get; set; } public bool Authenticated { get; set; } - public long TelegramChatId { get; set; } public DateTime Expiration { get; set; } } diff --git a/InvoiceReminder.Authentication/Jwt/JwtProvider.cs b/InvoiceReminder.Authentication/Jwt/JwtProvider.cs index 3114d52..36e7002 100644 --- a/InvoiceReminder.Authentication/Jwt/JwtProvider.cs +++ b/InvoiceReminder.Authentication/Jwt/JwtProvider.cs @@ -37,7 +37,9 @@ public JwtObject Generate(UserClaims user) var claims = new Claim[] { new (JwtRegisteredClaimNames.Sub, user.Id.ToString()), - new (JwtRegisteredClaimNames.Email, user.Email) + new (JwtRegisteredClaimNames.Name, user.Name), + new (JwtRegisteredClaimNames.Email, user.Email), + new ("telegramChatId", user.TelegramChatId.ToString()) }; var symmetricKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtOptions.SecretKey)); @@ -56,12 +58,8 @@ public JwtObject Generate(UserClaims user) return new JwtObject { - UserId = user.Id, - Name = user.Name, - Email = user.Email, AuthenticationToken = tokenHandler.WriteToken(token), Authenticated = true, - TelegramChatId = user.TelegramChatId, Expiration = token.ValidTo }; } diff --git a/InvoiceReminder.Data/Repository/UserRepository.cs b/InvoiceReminder.Data/Repository/UserRepository.cs index 3c822f1..e649d72 100644 --- a/InvoiceReminder.Data/Repository/UserRepository.cs +++ b/InvoiceReminder.Data/Repository/UserRepository.cs @@ -177,6 +177,18 @@ update invoice_reminder.user throw new OperationCanceledException(contextualInfo, ex, cancellationToken); } + catch (DbUpdateException ex) + { + var method = $"{nameof(UserRepository)}.{nameof(UpdateBasicUserInfoAsync)}"; + var contextualInfo = $"Database update failed while executing >> {method}(...)"; + + if (_logger.IsEnabled(LogLevel.Error)) + { + _logger.LogError(ex, LogExceptionMessage, contextualInfo, ex.Message); + } + + throw new DataLayerException(contextualInfo, ex); + } catch (Exception ex) { var method = $"{nameof(UserRepository)}.{nameof(UpdateBasicUserInfoAsync)}"; From dd8eb2a7590dcdd9e297e178970fd603d18fb65e Mon Sep 17 00:00:00 2001 From: "Jefferson L. da Silva" Date: Sun, 8 Feb 2026 18:27:10 -0300 Subject: [PATCH 3/3] Code review Fix --- .../AppServices/UserPasswordAppService.cs | 4 ++-- InvoiceReminder.Data/Repository/UserRepository.cs | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/InvoiceReminder.Application/AppServices/UserPasswordAppService.cs b/InvoiceReminder.Application/AppServices/UserPasswordAppService.cs index b152617..ba9f73f 100644 --- a/InvoiceReminder.Application/AppServices/UserPasswordAppService.cs +++ b/InvoiceReminder.Application/AppServices/UserPasswordAppService.cs @@ -101,8 +101,8 @@ public async Task> ChangePasswordAsync( var result = await _repository.ChangePasswordAsync(viewModel.Adapt(), cancellationToken); return result - ? Result.Failure("Failed to change the password.") - : Result.Success(result); + ? Result.Success(result) + : Result.Failure("Failed to change the password."); } public async Task> GetByUserIdAsync( diff --git a/InvoiceReminder.Data/Repository/UserRepository.cs b/InvoiceReminder.Data/Repository/UserRepository.cs index e649d72..94d74a7 100644 --- a/InvoiceReminder.Data/Repository/UserRepository.cs +++ b/InvoiceReminder.Data/Repository/UserRepository.cs @@ -7,6 +7,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using System.Data; +using System.Data.Common; namespace InvoiceReminder.Data.Repository; @@ -177,7 +178,7 @@ update invoice_reminder.user throw new OperationCanceledException(contextualInfo, ex, cancellationToken); } - catch (DbUpdateException ex) + catch (DbException ex) { var method = $"{nameof(UserRepository)}.{nameof(UpdateBasicUserInfoAsync)}"; var contextualInfo = $"Database update failed while executing >> {method}(...)";