diff --git a/src/TimeLogIdentityService/IdentityService.API/Controllers/IdentityController.cs b/src/TimeLogIdentityService/IdentityService.API/Controllers/IdentityController.cs deleted file mode 100644 index 6114442..0000000 --- a/src/TimeLogIdentityService/IdentityService.API/Controllers/IdentityController.cs +++ /dev/null @@ -1,42 +0,0 @@ -#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member - -namespace IdentityService.API.Controllers; - -[Route("api/[controller]")] -[ApiController] -public class IdentityController(IMediator mediator) : ControllerBase -{ - private readonly IMediator _mediator = mediator; - - [HttpPost] - [Route("CreateAccount")] - public async Task CreateAccount(CreateAccountCommand account) - { - IdentityResult user = await _mediator.Send(account); - return Ok(user); - } - - [HttpPost] - [Route("Login")] - public async Task Login(LoginCommand login) - { - ApiResponse user = await _mediator.Send(login); - return Ok(user); - } - - [HttpPost] - [Route("AddRole")] - public async Task AddRole(AddRoleCommand role) - { - IdentityResult roleResponse = await _mediator.Send(role); - return Ok(roleResponse); - } - - [HttpPost] - [Route("AttachUserToRole")] - public async Task AttachUserToRole(AttachUserToRoleCommand userToRole) - { - ApiResponse response = await _mediator.Send(userToRole); - return Ok(response); - } -} \ No newline at end of file diff --git a/src/TimeLogIdentityService/IdentityService.API/GlobalUsings.cs b/src/TimeLogIdentityService/IdentityService.API/GlobalUsings.cs index 5c12ba7..8a6c126 100644 --- a/src/TimeLogIdentityService/IdentityService.API/GlobalUsings.cs +++ b/src/TimeLogIdentityService/IdentityService.API/GlobalUsings.cs @@ -2,6 +2,7 @@ global using IdentityService.Application; global using IdentityService.Application.Features.AddRole; global using IdentityService.Application.Features.AttachUserToRole; +global using IdentityService.Application.Features.ChangePassword; global using IdentityService.Application.Features.CreateAccount; global using IdentityService.Application.Features.Login; global using IdentityService.Contracts; diff --git a/src/TimeLogIdentityService/IdentityService.API/IdentityEndPoint.cs b/src/TimeLogIdentityService/IdentityService.API/IdentityEndPoint.cs new file mode 100644 index 0000000..30d0c9c --- /dev/null +++ b/src/TimeLogIdentityService/IdentityService.API/IdentityEndPoint.cs @@ -0,0 +1,64 @@ +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + +namespace IdentityService.API; + +public static class IdentityEndPoint +{ + public static void AddEndpoints(this IEndpointRouteBuilder app) + { + _ = app.MapPost("/CreateAccount", async (IMediator _mediator, [FromBody] CreateAccountCommand request) => + { + IdentityResult userResponse = await _mediator.Send(request); + if (!userResponse.Succeeded) + { + return Results.BadRequest(userResponse); + } + + return Results.Ok(userResponse); + }); + + _ = app.MapPost("/Login", async (IMediator _mediator, [FromBody] LoginCommand request) => + { + ApiResponse userResponse = await _mediator.Send(request); + if (!userResponse.Succeeded) + { + return Results.BadRequest(userResponse); + } + + return Results.Ok(userResponse); + }); + + _ = app.MapPost("/AddRole", async (IMediator _mediator, [FromBody] AddRoleCommand request) => + { + IdentityResult roleResponse = await _mediator.Send(request); + if (!roleResponse.Succeeded) + { + return Results.BadRequest(roleResponse); + } + + return Results.Ok(roleResponse); + }); + + _ = app.MapPost("/AttachUserToRole", async (IMediator _mediator, [FromBody] AttachUserToRoleCommand request) => + { + ApiResponse response = await _mediator.Send(request); + if (!response.Succeeded) + { + return Results.BadRequest(response); + } + + return Results.Ok(response); + }); + + _ = app.MapPost("/ChangePassword", async (IMediator _mediator, [FromBody] ChangePasswordCommand request) => + { + ApiResponse response = await _mediator.Send(request); + if (!response.Succeeded) + { + return Results.BadRequest(response); + } + + return Results.Ok(response); + }); + } +} \ No newline at end of file diff --git a/src/TimeLogIdentityService/IdentityService.API/Program.cs b/src/TimeLogIdentityService/IdentityService.API/Program.cs index aacdd1b..c2c134b 100644 --- a/src/TimeLogIdentityService/IdentityService.API/Program.cs +++ b/src/TimeLogIdentityService/IdentityService.API/Program.cs @@ -41,6 +41,7 @@ _ = app.UseAuthorization(); _ = app.MapControllers(); + app.AddEndpoints(); app.Run(); } diff --git a/src/TimeLogIdentityService/IdentityService.Application/Features/AttachUserToRole/AttachUserToRoleCommandHandler.cs b/src/TimeLogIdentityService/IdentityService.Application/Features/AttachUserToRole/AttachUserToRoleCommandHandler.cs index e8b0045..1824b00 100644 --- a/src/TimeLogIdentityService/IdentityService.Application/Features/AttachUserToRole/AttachUserToRoleCommandHandler.cs +++ b/src/TimeLogIdentityService/IdentityService.Application/Features/AttachUserToRole/AttachUserToRoleCommandHandler.cs @@ -1,3 +1,5 @@ +using IdentityService.Contracts.Constant; + #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member namespace IdentityService.Application.Features.AttachUserToRole; @@ -18,8 +20,8 @@ public async Task> Handle( Error = new ProblemDetails() { Status = 404, - Title = "NotFound", - Detail = "User not found", + Title = nameof(ErrorDetails.UserNotFound), + Detail = ErrorDetails.UserNotFound, }, }; } @@ -29,16 +31,16 @@ public async Task> Handle( { return new ApiResponse() { - Success = false, + Succeeded = false, Error = new ProblemDetails() { Status = 400, - Title = "RoleAssignmentFailed", + Title = nameof(ErrorDetails.RoleAssignmentFailed), Detail = string.Join(", ", result.Errors.Select(e => e.Description)), }, }; } - return new ApiResponse() { Success = true, }; + return new ApiResponse() { Succeeded = true, }; } } \ No newline at end of file diff --git a/src/TimeLogIdentityService/IdentityService.Application/Features/ChangePassword/ChangePasswordCommand.cs b/src/TimeLogIdentityService/IdentityService.Application/Features/ChangePassword/ChangePasswordCommand.cs new file mode 100644 index 0000000..28bfa4d --- /dev/null +++ b/src/TimeLogIdentityService/IdentityService.Application/Features/ChangePassword/ChangePasswordCommand.cs @@ -0,0 +1,5 @@ +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +namespace IdentityService.Application.Features.ChangePassword +{ + public record class ChangePasswordCommand(string OldPassword, string NewPassword, string Email) : IRequest; +} \ No newline at end of file diff --git a/src/TimeLogIdentityService/IdentityService.Application/Features/ChangePassword/ChangePasswordCommandHandler.cs b/src/TimeLogIdentityService/IdentityService.Application/Features/ChangePassword/ChangePasswordCommandHandler.cs new file mode 100644 index 0000000..37af55f --- /dev/null +++ b/src/TimeLogIdentityService/IdentityService.Application/Features/ChangePassword/ChangePasswordCommandHandler.cs @@ -0,0 +1,50 @@ +using IdentityService.Contracts.Constant; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + +namespace IdentityService.Application.Features.ChangePassword +{ + public class ChangePasswordCommandHandler(UserManager userManager) : + IRequestHandler + { + private readonly UserManager _userManager = userManager; + + public async Task Handle(ChangePasswordCommand request, CancellationToken cancellationToken) + { + IdentityUser? user = await _userManager.FindByEmailAsync(request.Email); + if (user == null) + { + return new ApiResponse() + { + Succeeded = false, + Error = new ProblemDetails() + { + Title = nameof(ErrorDetails.InvalidEmail), + Detail = ErrorDetails.InvalidEmail, + Status = 404, + }, + }; + } + + if (!await _userManager.CheckPasswordAsync(user, request.OldPassword)) + { + return new ApiResponse() + { + Succeeded = false, + Error = new ProblemDetails() + { + Title = nameof(ErrorDetails.InvalidOldPassword), + Detail = ErrorDetails.InvalidOldPassword, + Status = 400, + }, + }; + } + + _ = await _userManager.ChangePasswordAsync(user, request.OldPassword, request.NewPassword); + return new ApiResponse() + { + Succeeded = false, + }; + } + } +} \ No newline at end of file diff --git a/src/TimeLogIdentityService/IdentityService.Application/Features/Login/LoginCommandHandler.cs b/src/TimeLogIdentityService/IdentityService.Application/Features/Login/LoginCommandHandler.cs index 5ba42fe..89612c6 100644 --- a/src/TimeLogIdentityService/IdentityService.Application/Features/Login/LoginCommandHandler.cs +++ b/src/TimeLogIdentityService/IdentityService.Application/Features/Login/LoginCommandHandler.cs @@ -1,3 +1,5 @@ +using IdentityService.Contracts.Constant; + #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member namespace IdentityService.Application.Features.Login; @@ -14,11 +16,11 @@ public async Task> Handle(LoginCommand request, Cance { return new ApiResponse() { - Success = false, + Succeeded = false, Error = new ProblemDetails() { - Title = "InvalidEmail", - Detail = $"Email {request.Email} is invalid", + Title = nameof(ErrorDetails.InvalidEmail), + Detail = ErrorDetails.InvalidEmail, Status = 400, }, }; @@ -29,11 +31,11 @@ public async Task> Handle(LoginCommand request, Cance { return new ApiResponse() { - Success = false, + Succeeded = false, Error = new ProblemDetails() { - Title = "InvalidPassword", - Detail = "Password is invalid", + Title = nameof(ErrorDetails.InvalidPassword), + Detail = ErrorDetails.InvalidPassword, Status = 400, }, }; @@ -65,7 +67,7 @@ public async Task> Handle(LoginCommand request, Cance return new ApiResponse() { - Success = true, + Succeeded = true, Data = new LoginResponse() { Token = new JwtSecurityTokenHandler().WriteToken(token) }, }; } diff --git a/src/TimeLogIdentityService/IdentityService.Contracts/ApiResponse.cs b/src/TimeLogIdentityService/IdentityService.Contracts/ApiResponse.cs index 0e46742..befa6d0 100644 --- a/src/TimeLogIdentityService/IdentityService.Contracts/ApiResponse.cs +++ b/src/TimeLogIdentityService/IdentityService.Contracts/ApiResponse.cs @@ -4,11 +4,7 @@ namespace IdentityService.Contracts; -public class ApiResponse +public class ApiResponse(T? data = default) : ApiResponse(data) { - public bool Success { get; set; } - - public T? Data { get; set; } - - public ProblemDetails? Error { get; set; } = new(); + public new T? Data { get; set; } = data; } \ No newline at end of file diff --git a/src/TimeLogIdentityService/IdentityService.Contracts/BaseResponse.cs b/src/TimeLogIdentityService/IdentityService.Contracts/BaseResponse.cs new file mode 100644 index 0000000..2b10b3f --- /dev/null +++ b/src/TimeLogIdentityService/IdentityService.Contracts/BaseResponse.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.Mvc; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +namespace IdentityService.Contracts +{ + public class ApiResponse(object? data = null) + { + public bool Succeeded { get; set; } + + public object? Data { get; set; } = data; + + public ProblemDetails? Error { get; set; } = new(); + } +} \ No newline at end of file diff --git a/src/TimeLogIdentityService/IdentityService.Contracts/Constant/ErrorDetails.cs b/src/TimeLogIdentityService/IdentityService.Contracts/Constant/ErrorDetails.cs new file mode 100644 index 0000000..24395b9 --- /dev/null +++ b/src/TimeLogIdentityService/IdentityService.Contracts/Constant/ErrorDetails.cs @@ -0,0 +1,25 @@ +using System.ComponentModel.DataAnnotations; +using System.Reflection; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +namespace IdentityService.Contracts.Constant +{ + public static class ErrorDetails + { + public const string InvalidUserName = "Invalid UserName"; + + public const string InvalidPassword = "Invalid Password"; + + public const string InvalidEmail = "Invalid Email"; + + public const string InvalidUserId = "Invalid UserId"; + + public const string InvalidOldPassword = "Invalid OldPassword"; + + public const string UserNotFound = "User Not Found"; + + public const string RoleNotFound = "Role Not Found"; + + public const string RoleAssignmentFailed = "Role Assignment Failed"; + } +} \ No newline at end of file