Skip to content

Commit c0fc6ed

Browse files
authored
feat: implement soft delete for users and roles (#17)
1 parent 0fe7459 commit c0fc6ed

22 files changed

+564
-131
lines changed

src/Api/Api.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
<ItemGroup>
1010
<PackageReference Include="AspNetCore.HealthChecks.UI" Version="9.0.0" />
11+
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.2" />
1112
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.10" />
1213
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.13">
1314
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@@ -21,6 +22,8 @@
2122
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.1.0" />
2223
<PackageReference Include="AspNetCore.HealthChecks.UI.Client" Version="9.0.0" />
2324
<PackageReference Include="AspNetCore.HealthChecks.UI.InMemory.Storage" Version="9.0.0" />
25+
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="7.2.0" />
26+
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.1.2" />
2427
</ItemGroup>
2528

2629
<ItemGroup>

src/Api/Controllers/RoleController.cs

Lines changed: 44 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using Api.Models.Common;
2+
using Api.Models.Enums;
13
using Core.Entities.DTOs;
24
using Core.Services.Interfaces;
35
using Microsoft.AspNetCore.Mvc;
@@ -16,67 +18,76 @@ public RoleController(IRoleService roleService)
1618
}
1719

1820
[HttpGet("{id}")]
19-
public async Task<IActionResult> GetRoleById(int id)
21+
public async Task<ActionResult<BaseResponse<RoleResponseDto>>> GetRoleById(int id)
2022
{
23+
BaseResponse<RoleResponseDto> response = new(ResponseStatus.Success);
2124
RoleResponseDto? role = await _roleService.GetRoleByIdAsync(id).ConfigureAwait(false);
22-
if (role == null)
23-
{
24-
return NotFound(new { message = "Role not found" });
25-
}
26-
27-
return Ok(role);
25+
response.Data = role;
26+
response.Message = "Role retrieved successfully";
27+
return Ok(response);
2828
}
2929

3030
[HttpGet]
31-
public async Task<IActionResult> GetAllRoles()
31+
public async Task<ActionResult<BaseResponse<IEnumerable<RoleResponseDto>>>> GetAllRoles([FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 10)
3232
{
33-
IEnumerable<RoleResponseDto> roles = await _roleService.GetAllRolesAsync().ConfigureAwait(false);
34-
return Ok(roles);
33+
BaseResponse<IEnumerable<RoleResponseDto>> response = new(ResponseStatus.Success);
34+
IEnumerable<RoleResponseDto> roles = await _roleService.GetAllRolesAsync(pageNumber, pageSize).ConfigureAwait(false);
35+
int totalCount = await _roleService.GetTotalRolesCountAsync().ConfigureAwait(false);
36+
int totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
37+
38+
response.Data = roles;
39+
response.Message = "Roles retrieved successfully";
40+
response.Pagination = new PaginationMetadata
41+
{
42+
PageNumber = pageNumber,
43+
PageSize = pageSize,
44+
TotalCount = totalCount,
45+
TotalPages = totalPages,
46+
HasPreviousPage = pageNumber > 1,
47+
HasNextPage = pageNumber < totalPages
48+
};
49+
return Ok(response);
3550
}
3651

3752
[HttpPost]
38-
public async Task<IActionResult> CreateRole([FromBody] RoleCreateDto roleDto)
53+
public async Task<ActionResult<BaseResponse<RoleResponseDto?>>> CreateRole([FromBody] RoleCreateDto roleDto)
3954
{
55+
BaseResponse<RoleResponseDto> response = new(ResponseStatus.Success);
4056
if (!ModelState.IsValid)
4157
{
42-
return BadRequest(ModelState);
58+
return ModelValidationBadRequest.GenerateErrorResponse(ModelState);
4359
}
4460

4561
RoleResponseDto? createdRole = await _roleService.CreateRoleAsync(roleDto).ConfigureAwait(false);
46-
if (createdRole == null)
47-
{
48-
return BadRequest(new { message = "Failed to create role" });
49-
}
50-
51-
return CreatedAtAction(nameof(GetRoleById), new { id = createdRole.Id }, createdRole);
62+
response.Data = createdRole;
63+
response.Message = "Role created successfully";
64+
return Ok(response);
5265
}
5366

5467
[HttpPut("{id}")]
55-
public async Task<IActionResult> UpdateRole(int id, [FromBody] RoleUpdateDto roleDto)
68+
public async Task<ActionResult<BaseResponse<RoleResponseDto>>> UpdateRole(int id, [FromBody] RoleUpdateDto roleDto)
5669
{
70+
BaseResponse<RoleResponseDto> response = new(ResponseStatus.Success);
5771
if (!ModelState.IsValid)
5872
{
59-
return BadRequest(ModelState);
73+
return ModelValidationBadRequest.GenerateErrorResponse(ModelState);
6074
}
6175

62-
bool success = await _roleService.UpdateRoleAsync(id, roleDto).ConfigureAwait(false);
63-
if (!success)
64-
{
65-
return NotFound(new { message = "Role not found or update failed" });
66-
}
76+
RoleResponseDto? updatedRole = await _roleService.UpdateRoleAsync(id, roleDto).ConfigureAwait(false);
6777

68-
return NoContent();
78+
response.Data = updatedRole;
79+
response.Message = "Role updated successfully";
80+
return Ok(response);
6981
}
7082

7183
[HttpDelete("{id}")]
72-
public async Task<IActionResult> DeleteRole(int id)
84+
public async Task<ActionResult<BaseResponse<bool>>> DeleteRole(int id)
7385
{
74-
bool success = await _roleService.DeleteRoleAsync(id).ConfigureAwait(false);
75-
if (!success)
76-
{
77-
return NotFound(new { message = "Role not found or deletion failed" });
78-
}
86+
BaseResponse<bool> response = new(ResponseStatus.Success);
87+
bool isDeleted = await _roleService.DeleteRoleAsync(id).ConfigureAwait(false);
7988

80-
return NoContent();
89+
response.Data = isDeleted;
90+
response.Message = "Role deactivated successfully";
91+
return Ok(response);
8192
}
8293
}

src/Api/Controllers/UserController.cs

Lines changed: 71 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
using Api.Models.Common;
2+
using Api.Models.Enums;
13
using Core.Entities.DTOs;
24
using Core.Services.Interfaces;
5+
using Microsoft.AspNetCore.Authorization;
36
using Microsoft.AspNetCore.Mvc;
47

58
namespace Api.Controllers;
@@ -15,55 +18,105 @@ public UserController(IUserService userService)
1518
_userService = userService;
1619
}
1720

21+
[Authorize]
1822
[HttpGet("{id}")]
19-
public async Task<IActionResult> GetUserById(int id)
23+
public async Task<ActionResult<BaseResponse<UserResponseDto>>> GetUserById(int id)
2024
{
25+
BaseResponse<UserResponseDto> response = new(ResponseStatus.Success);
2126
UserResponseDto? user = await _userService.GetUserByIdAsync(id).ConfigureAwait(false);
22-
return user == null ? NotFound(new { message = "User not found" }) : Ok(user);
27+
28+
response.Data = user;
29+
response.Message = "User retrieved successfully";
30+
return Ok(response);
2331
}
2432

33+
[Authorize]
2534
[HttpGet("email/{email}")]
26-
public async Task<IActionResult> GetUserByEmail(string email)
35+
public async Task<ActionResult<BaseResponse<UserResponseDto>>> GetUserByEmail(string email)
2736
{
37+
BaseResponse<UserResponseDto> response = new(ResponseStatus.Success);
2838
UserResponseDto? user = await _userService.GetUserByEmailAsync(email).ConfigureAwait(false);
29-
return user == null ? NotFound(new { message = "User not found" }) : Ok(user);
39+
40+
response.Data = user;
41+
response.Message = "User retrieved successfully";
42+
return Ok(response);
3043
}
3144

45+
[Authorize(Roles = "Admin")]
3246
[HttpGet]
33-
public async Task<IActionResult> GetAllUsers()
47+
public async Task<ActionResult<BaseResponse<IEnumerable<UserResponseDto>>>> GetAllUsers([FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 10)
48+
{
49+
BaseResponse<IEnumerable<UserResponseDto>> response = new(ResponseStatus.Success);
50+
IEnumerable<UserResponseDto> users = await _userService.GetAllUsersAsync(pageNumber, pageSize).ConfigureAwait(false);
51+
int totalCount = await _userService.GetTotalUsersCountAsync().ConfigureAwait(false);
52+
int totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
53+
54+
response.Data = users;
55+
response.Message = "Users retrieved successfully";
56+
response.Pagination = new PaginationMetadata
57+
{
58+
PageNumber = pageNumber,
59+
PageSize = pageSize,
60+
TotalCount = totalCount,
61+
TotalPages = totalPages,
62+
HasPreviousPage = pageNumber > 1,
63+
HasNextPage = pageNumber < totalPages
64+
};
65+
return Ok(response);
66+
}
67+
68+
[HttpPost("login")]
69+
public async Task<ActionResult<BaseResponse<string>>> Login([FromBody] LoginRequestDto loginDto)
3470
{
35-
IEnumerable<UserResponseDto> users = await _userService.GetAllUsersAsync().ConfigureAwait(false);
36-
return Ok(users);
71+
BaseResponse<string> response = new(ResponseStatus.Success);
72+
string? token = await _userService.Login(loginDto.Email, loginDto.Password).ConfigureAwait(false);
73+
response.Data = token;
74+
response.Message = "Login Successful";
75+
return Ok(response);
3776
}
3877

78+
3979
[HttpPost("register")]
40-
public async Task<IActionResult> RegisterUser([FromBody] UserCreateDto userDto)
80+
public async Task<ActionResult<BaseResponse<UserResponseDto>>> RegisterUser([FromBody] UserCreateDto userDto)
4181
{
82+
BaseResponse<UserResponseDto> response = new(ResponseStatus.Success);
4283
if (!ModelState.IsValid)
4384
{
44-
return BadRequest(ModelState);
85+
return ModelValidationBadRequest.GenerateErrorResponse(ModelState);
4586
}
87+
UserResponseDto? createdUser = await _userService.AddUserAsync(userDto).ConfigureAwait(false);
4688

47-
bool success = await _userService.AddUserAsync(userDto).ConfigureAwait(false);
48-
return success ? StatusCode(201, new { message = "User registered successfully" }) : BadRequest(new { message = "Failed to register user" });
89+
response.Data = createdUser;
90+
response.Message = "User registered successfully";
91+
return Ok(response);
4992
}
5093

94+
[Authorize]
5195
[HttpPut("{id}")]
52-
public async Task<IActionResult> UpdateUser(int id, [FromBody] UserUpdateDto userDto)
96+
public async Task<ActionResult<BaseResponse<UserResponseDto>>> UpdateUser(int id, [FromBody] UserUpdateDto userDto)
5397
{
98+
BaseResponse<UserResponseDto> response = new(ResponseStatus.Success);
5499
if (!ModelState.IsValid)
55100
{
56-
return BadRequest(ModelState);
101+
return ModelValidationBadRequest.GenerateErrorResponse(ModelState);
57102
}
58103

59-
bool success = await _userService.UpdateUserAsync(id, userDto).ConfigureAwait(false);
60-
return success ? NoContent() : NotFound(new { message = "User not found or update failed" });
104+
UserResponseDto? updatedUser = await _userService.UpdateUserAsync(id, userDto).ConfigureAwait(false);
105+
106+
response.Data = updatedUser;
107+
response.Message = "User updated successfully";
108+
return Ok(response);
61109
}
62110

111+
[Authorize(Roles = "Admin")]
63112
[HttpDelete("{id}")]
64-
public async Task<IActionResult> DeleteUser(int id)
113+
public async Task<ActionResult<BaseResponse<bool>>> DeleteUser(int id)
65114
{
66-
bool success = await _userService.DeleteUserAsync(id).ConfigureAwait(false);
67-
return success ? NoContent() : NotFound(new { message = "User not found or deletion failed" });
115+
BaseResponse<bool> response = new(ResponseStatus.Success);
116+
bool isDeleted = await _userService.DeleteUserAsync(id).ConfigureAwait(false);
117+
118+
response.Data = isDeleted;
119+
response.Message = "User deactivated successfully";
120+
return Ok(response);
68121
}
69122
}

src/Api/Filters/CustomExceptionFilter.cs

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,6 @@
55
using Core.Exceptions;
66
using Microsoft.AspNetCore.Http;
77

8-
// /*
9-
// Added to suppress the following warning :
10-
// Warning : Add a public read-only property accessor for positional argument logger of Attribute CustomExceptionFilter
11-
// */
12-
// #pragma warning disable CA1019
13-
148
namespace Api.Filters;
159

1610
public class CustomExceptionFilter : ExceptionFilterAttribute
@@ -24,26 +18,28 @@ public CustomExceptionFilter(ILogger<CustomExceptionFilter> logger)
2418

2519
public override void OnException(ExceptionContext context)
2620
{
27-
BaseResponse<int> response = new BaseResponse<int>(ResponseStatus.Error);
21+
BaseResponse<int> response = new BaseResponse<int>(ResponseStatus.Fail);
2822

29-
(int statusCode, string message) = context.Exception switch
23+
(int statusCode, string message, ResponseStatus status) = context.Exception switch
3024
{
31-
UnauthorizedAccessException ex => (StatusCodes.Status401Unauthorized, ex.Message),
32-
NotFoundException ex => (StatusCodes.Status404NotFound, ex.Message),
33-
BadRequestException ex => (StatusCodes.Status400BadRequest, ex.Message),
34-
AlreadyExistsException ex => (StatusCodes.Status409Conflict, ex.Message),
25+
UnauthorizedAccessException ex => (StatusCodes.Status401Unauthorized, ex.Message, ResponseStatus.Fail),
26+
NotFoundException ex => (StatusCodes.Status404NotFound, ex.Message, ResponseStatus.Fail),
27+
BadRequestException ex => (StatusCodes.Status400BadRequest, ex.Message, ResponseStatus.Fail),
28+
AlreadyExistsException ex => (StatusCodes.Status409Conflict, ex.Message, ResponseStatus.Fail),
29+
DatabaseOperationException ex => (StatusCodes.Status500InternalServerError, ex.Message, ResponseStatus.Error),
3530
_ => HandleUnexpectedException(context.Exception)
3631
};
32+
response.Status = status;
3733
response.Message = message;
3834
context.Result = new JsonResult(response)
3935
{
4036
StatusCode = statusCode
4137
};
4238
}
4339

44-
private (int StatusCode, string Message) HandleUnexpectedException(Exception ex)
40+
private (int StatusCode, string Message, ResponseStatus status) HandleUnexpectedException(Exception ex)
4541
{
4642
_logger.LogError(ex, "An unexpected error occurred");
47-
return (StatusCodes.Status500InternalServerError, "An unexpected error occurred. Please try again later.");
43+
return (StatusCodes.Status500InternalServerError, "An unexpected error occurred. Please try again later.", ResponseStatus.Error);
4844
}
4945
}

src/Api/Models/Common/BaseResponse.cs

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Microsoft.AspNetCore.Mvc;
22
using Api.Models.Enums;
3+
using Microsoft.AspNetCore.Mvc.ModelBinding;
34

45
namespace Api.Models.Common;
56

@@ -52,48 +53,56 @@ public class BaseResponse<T>
5253
public class ModelValidationBadRequest
5354
{
5455
/// <summary>
55-
/// Generates a BadRequestObjectResult based on the model state errors.
56+
/// Generates a standardized BadRequest response with model validation errors.
5657
/// </summary>
57-
/// <param name="actionContext">The action context containing the model state.</param>
58+
/// <param name="modelState">The model state containing validation errors.</param>
5859
/// <returns>A BadRequestObjectResult encapsulating the validation errors.</returns>
59-
public static BadRequestObjectResult ModelValidationErrorResponse(ActionContext actionContext)
60+
public static BadRequestObjectResult GenerateErrorResponse(ModelStateDictionary modelState)
6061
{
6162
return new BadRequestObjectResult(new BaseResponse<int>(ResponseStatus.Error)
6263
{
63-
Errors = actionContext.ModelState
64-
.Where(modelError => modelError.Value != null && modelError.Value.Errors.Any())
64+
Errors = modelState
65+
.Where(entry => entry.Value != null && entry.Value.Errors.Any())
6566
.ToDictionary(
66-
modelError => modelError.Key,
67-
modelError => modelError.Value != null
68-
? modelError.Value.Errors.Select(e => e.ErrorMessage).ToList()
69-
: new List<string>()
67+
entry => entry.Key,
68+
entry => entry.Value!.Errors.Select(error => error.ErrorMessage).ToList()
7069
)
7170
});
7271
}
7372
}
7473

7574
/// <summary>
76-
/// Provides utilities for generating bad request responses based on model validation errors.
75+
/// Represents metadata for paginated responses, including page details and navigation indicators.
7776
/// </summary>
7877
public class PaginationMetadata
7978
{
79+
/// <summary>
80+
/// Gets or sets the current page number.
81+
/// </summary>
82+
public int PageNumber { get; set; }
83+
8084
/// <summary>
8185
/// Gets or sets the number of items per page.
8286
/// </summary>
8387
public int PageSize { get; set; }
8488

8589
/// <summary>
86-
/// Gets or sets the total count of items (if available).
90+
/// Gets or sets the total number of items.
8791
/// </summary>
88-
public int? TotalCount { get; set; }
92+
public int TotalCount { get; set; }
8993

9094
/// <summary>
91-
/// Gets or sets the cursor for the previous page.
95+
/// Gets or sets the total number of pages.
9296
/// </summary>
93-
public DateTime? PrevCursor { get; set; }
97+
public int TotalPages { get; set; }
9498

9599
/// <summary>
96-
/// Gets or sets the cursor for the next page.
100+
/// Gets or sets a value indicating whether there is a previous page.
97101
/// </summary>
98-
public DateTime? NextCursor { get; set; }
99-
}
102+
public bool HasPreviousPage { get; set; }
103+
104+
/// <summary>
105+
/// Gets or sets a value indicating whether there is a next page.
106+
/// </summary>
107+
public bool HasNextPage { get; set; }
108+
}

0 commit comments

Comments
 (0)