// File: UsersController.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;
using th3chris.API.Models;
using th3chris.API.Services;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using System.Security.Claims;
using AutoMapper;
using FluentValidation;
namespace th3chris.API.Controllers
{
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class UsersController : ControllerBase
{
private readonly IUserService _userService;
private readonly ILogger<UsersController> _logger;
private readonly IMapper _mapper;
private readonly IValidator<CreateUserRequest> _createValidator;
private readonly IValidator<UpdateUserRequest> _updateValidator;
public UsersController(
IUserService userService,
ILogger<UsersController> logger,
IMapper mapper,
IValidator<CreateUserRequest> createValidator,
IValidator<UpdateUserRequest> updateValidator)
{
_userService = userService ?? throw new ArgumentNullException(nameof(userService));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
_createValidator = createValidator;
_updateValidator = updateValidator;
}
/// <summary>
/// Retrieves all users with pagination support
/// </summary>
/// <param name="pageNumber">Page number (default: 1)</param>
/// <param name="pageSize">Page size (default: 10, max: 100)</param>
/// <returns>Paginated list of users</returns>
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<ActionResult<PagedResult<UserDto>>> GetUsers(
[FromQuery] int pageNumber = 1,
[FromQuery] int pageSize = 10,
[FromQuery] string? search = null,
[FromQuery] string? sortBy = null,
[FromQuery] bool sortDescending = false)
{
try
{
if (pageNumber < 1)
return BadRequest("Page number must be greater than 0");
if (pageSize < 1 || pageSize > 100)
return BadRequest("Page size must be between 1 and 100");
_logger.LogInformation("Getting users - Page: {PageNumber}, Size: {PageSize}, Search: {Search}",
pageNumber, pageSize, search);
var currentUserId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var filter = new UserFilter
{
PageNumber = pageNumber,
PageSize = pageSize,
Search = search,
SortBy = sortBy,
SortDescending = sortDescending,
RequestingUserId = currentUserId
};
var users = await _userService.GetUsersAsync(filter);
var userDtos = _mapper.Map<IEnumerable<UserDto>>(users.Items);
var result = new PagedResult<UserDto>
{
Items = userDtos,
TotalCount = users.TotalCount,
PageNumber = pageNumber,
PageSize = pageSize,
TotalPages = (int)Math.Ceiling((double)users.TotalCount / pageSize)
};
return Ok(result);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occurred while getting users");
return StatusCode(StatusCodes.Status500InternalServerError,
"An error occurred while processing your request");
}
}
/// <summary>
/// Retrieves a specific user by ID
/// </summary>
/// <param name="id">User ID</param>
/// <returns>User details</returns>
[HttpGet("{id:int}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<ActionResult<UserDetailDto>> GetUser(int id)
{
try
{
if (id <= 0)
return BadRequest("Invalid user ID");
var currentUserId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
_logger.LogInformation("Getting user {UserId} requested by {CurrentUserId}", id, currentUserId);
var user = await _userService.GetUserByIdAsync(id, currentUserId);
if (user == null)
{
_logger.LogWarning("User {UserId} not found or access denied for {CurrentUserId}", id, currentUserId);
return NotFound($"User with ID {id} not found");
}
var userDto = _mapper.Map<UserDetailDto>(user);
return Ok(userDto);
}
catch (UnauthorizedAccessException ex)
{
_logger.LogWarning(ex, "Unauthorized access attempt for user {UserId}", id);
return Forbid();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occurred while getting user {UserId}", id);
return StatusCode(StatusCodes.Status500InternalServerError,
"An error occurred while processing your request");
}
}
/// <summary>
/// Creates a new user
/// </summary>
/// <param name="request">User creation request</param>
/// <returns>Created user</returns>
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
public async Task<ActionResult<UserDto>> CreateUser([FromBody] CreateUserRequest request)
{
try
{
if (request == null)
return BadRequest("Request body is required");
var validationResult = await _createValidator.ValidateAsync(request);
if (!validationResult.IsValid)
{
var errors = validationResult.Errors.Select(e => e.ErrorMessage);
return BadRequest(new { Errors = errors });
}
var currentUserId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
_logger.LogInformation("Creating new user {Email} by {CurrentUserId}", request.Email, currentUserId);
var user = _mapper.Map<User>(request);
user.CreatedBy = currentUserId;
user.CreatedAt = DateTime.UtcNow;
var createdUser = await _userService.CreateUserAsync(user);
var userDto = _mapper.Map<UserDto>(createdUser);
return CreatedAtAction(nameof(GetUser), new { id = createdUser.Id }, userDto);
}
catch (DuplicateUserException ex)
{
_logger.LogWarning(ex, "Attempt to create duplicate user {Email}", request?.Email);
return Conflict(new { Message = "User with this email already exists" });
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occurred while creating user");
return StatusCode(StatusCodes.Status500InternalServerError,
"An error occurred while processing your request");
}
}
/// <summary>
/// Updates an existing user
/// </summary>
/// <param name="id">User ID</param>
/// <param name="request">User update request</param>
/// <returns>Updated user</returns>
[HttpPut("{id:int}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<ActionResult<UserDto>> UpdateUser(int id, [FromBody] UpdateUserRequest request)
{
try
{
if (id <= 0)
return BadRequest("Invalid user ID");
if (request == null)
return BadRequest("Request body is required");
var validationResult = await _updateValidator.ValidateAsync(request);
if (!validationResult.IsValid)
{
var errors = validationResult.Errors.Select(e => e.ErrorMessage);
return BadRequest(new { Errors = errors });
}
var currentUserId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
_logger.LogInformation("Updating user {UserId} by {CurrentUserId}", id, currentUserId);
var existingUser = await _userService.GetUserByIdAsync(id, currentUserId);
if (existingUser == null)
{
_logger.LogWarning("User {UserId} not found for update by {CurrentUserId}", id, currentUserId);
return NotFound($"User with ID {id} not found");
}
_mapper.Map(request, existingUser);
existingUser.UpdatedBy = currentUserId;
existingUser.UpdatedAt = DateTime.UtcNow;
var updatedUser = await _userService.UpdateUserAsync(existingUser);
var userDto = _mapper.Map<UserDto>(updatedUser);
return Ok(userDto);
}
catch (UnauthorizedAccessException ex)
{
_logger.LogWarning(ex, "Unauthorized update attempt for user {UserId}", id);
return Forbid();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occurred while updating user {UserId}", id);
return StatusCode(StatusCodes.Status500InternalServerError,
"An error occurred while processing your request");
}
}
/// <summary>
/// Deletes a user by ID
/// </summary>
/// <param name="id">User ID</param>
/// <returns>No content on success</returns>
[HttpDelete("{id:int}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<IActionResult> DeleteUser(int id)
{
try
{
if (id <= 0)
return BadRequest("Invalid user ID");
var currentUserId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
_logger.LogInformation("Deleting user {UserId} by {CurrentUserId}", id, currentUserId);
var success = await _userService.DeleteUserAsync(id, currentUserId);
if (!success)
{
_logger.LogWarning("User {UserId} not found for deletion by {CurrentUserId}", id, currentUserId);
return NotFound($"User with ID {id} not found");
}
_logger.LogInformation("User {UserId} successfully deleted by {CurrentUserId}", id, currentUserId);
return NoContent();
}
catch (UnauthorizedAccessException ex)
{
_logger.LogWarning(ex, "Unauthorized deletion attempt for user {UserId}", id);
return Forbid();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occurred while deleting user {UserId}", id);
return StatusCode(StatusCodes.Status500InternalServerError,
"An error occurred while processing your request");
}
}
}
}
// File: UserService.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using th3chris.API.Models;
using th3chris.API.Data;
using th3chris.API.Exceptions;
namespace th3chris.API.Services
{
public class UserService : IUserService
{
private readonly AppDbContext _context;
private readonly ILogger<UserService> _logger;
public UserService(AppDbContext context, ILogger<UserService> logger)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task<PagedResult<User>> GetUsersAsync(UserFilter filter)
{
var query = _context.Users.AsQueryable();
if (!string.IsNullOrWhiteSpace(filter.Search))
{
var searchTerm = filter.Search.ToLower();
query = query.Where(u => u.Email.ToLower().Contains(searchTerm) ||
u.FirstName.ToLower().Contains(searchTerm) ||
u.LastName.ToLower().Contains(searchTerm));
}
var totalCount = await query.CountAsync();
if (!string.IsNullOrWhiteSpace(filter.SortBy))
{
query = ApplySorting(query, filter.SortBy, filter.SortDescending);
}
else
{
query = query.OrderBy(u => u.LastName).ThenBy(u => u.FirstName);
}
var users = await query
.Skip((filter.PageNumber - 1) * filter.PageSize)
.Take(filter.PageSize)
.ToListAsync();
return new PagedResult<User>
{
Items = users,
TotalCount = totalCount
};
}
}
}