feat: Add UserController and user-related DTOs for user management functionality
This commit is contained in:
483
Api/Controllers/UserController.cs
Normal file
483
Api/Controllers/UserController.cs
Normal file
@@ -0,0 +1,483 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Api.Models;
|
||||
using Api.Models.DTOs;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace Api.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("api/[controller]")]
|
||||
public class UserController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _context;
|
||||
private readonly ILogger<UserController> _logger;
|
||||
|
||||
public UserController(AppDbContext context, ILogger<UserController> logger)
|
||||
{
|
||||
_context = context;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all users with pagination support
|
||||
/// </summary>
|
||||
/// <param name="page">Page number (default: 1)</param>
|
||||
/// <param name="pageSize">Page size (default: 10, max: 100)</param>
|
||||
/// <param name="search">Search term for email, first name, or last name</param>
|
||||
/// <param name="isActive">Filter by active status</param>
|
||||
/// <returns>Paginated list of users</returns>
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<object>> GetUsers(
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 10,
|
||||
[FromQuery] string? search = null,
|
||||
[FromQuery] bool? isActive = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Validate pagination parameters
|
||||
page = Math.Max(1, page);
|
||||
pageSize = Math.Min(100, Math.Max(1, pageSize));
|
||||
|
||||
var query = _context.Users.Include(u => u.OAuthProviders).AsQueryable();
|
||||
|
||||
// Apply filters
|
||||
if (isActive.HasValue)
|
||||
{
|
||||
query = query.Where(u => u.IsActive == isActive.Value);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(search))
|
||||
{
|
||||
var searchTerm = search.ToLower();
|
||||
query = query.Where(u =>
|
||||
u.Email.ToLower().Contains(searchTerm) ||
|
||||
(u.FirstName != null && u.FirstName.ToLower().Contains(searchTerm)) ||
|
||||
(u.LastName != null && u.LastName.ToLower().Contains(searchTerm)));
|
||||
}
|
||||
|
||||
var totalCount = await query.CountAsync();
|
||||
var users = await query
|
||||
.OrderByDescending(u => u.CreatedAt)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.Select(u => new UserDto
|
||||
{
|
||||
Id = u.Id,
|
||||
Email = u.Email,
|
||||
FirstName = u.FirstName,
|
||||
LastName = u.LastName,
|
||||
ProfilePictureUrl = u.ProfilePictureUrl,
|
||||
CreatedAt = u.CreatedAt,
|
||||
LastLoginAt = u.LastLoginAt,
|
||||
IsActive = u.IsActive,
|
||||
OAuthProviders = u.OAuthProviders.Select(op => new UserOAuthProviderDto
|
||||
{
|
||||
Id = op.Id,
|
||||
Provider = op.Provider,
|
||||
ProviderId = op.ProviderId,
|
||||
ProviderEmail = op.ProviderEmail,
|
||||
ProviderName = op.ProviderName,
|
||||
CreatedAt = op.CreatedAt,
|
||||
LastUsedAt = op.LastUsedAt
|
||||
}).ToList()
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
users,
|
||||
pagination = new
|
||||
{
|
||||
page,
|
||||
pageSize,
|
||||
totalCount,
|
||||
totalPages = (int)Math.Ceiling(totalCount / (double)pageSize),
|
||||
hasNext = page * pageSize < totalCount,
|
||||
hasPrevious = page > 1
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error retrieving users");
|
||||
return StatusCode(500, "Internal server error while retrieving users");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a specific user by ID
|
||||
/// </summary>
|
||||
/// <param name="id">User ID</param>
|
||||
/// <returns>User details</returns>
|
||||
[HttpGet("{id}")]
|
||||
public async Task<ActionResult<UserDto>> GetUser(int id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var user = await _context.Users
|
||||
.Include(u => u.OAuthProviders)
|
||||
.FirstOrDefaultAsync(u => u.Id == id);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"User with ID {id} not found");
|
||||
}
|
||||
|
||||
var userDto = new UserDto
|
||||
{
|
||||
Id = user.Id,
|
||||
Email = user.Email,
|
||||
FirstName = user.FirstName,
|
||||
LastName = user.LastName,
|
||||
ProfilePictureUrl = user.ProfilePictureUrl,
|
||||
CreatedAt = user.CreatedAt,
|
||||
LastLoginAt = user.LastLoginAt,
|
||||
IsActive = user.IsActive,
|
||||
OAuthProviders = user.OAuthProviders.Select(op => new UserOAuthProviderDto
|
||||
{
|
||||
Id = op.Id,
|
||||
Provider = op.Provider,
|
||||
ProviderId = op.ProviderId,
|
||||
ProviderEmail = op.ProviderEmail,
|
||||
ProviderName = op.ProviderName,
|
||||
CreatedAt = op.CreatedAt,
|
||||
LastUsedAt = op.LastUsedAt
|
||||
}).ToList()
|
||||
};
|
||||
|
||||
return Ok(userDto);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error retrieving user with ID {UserId}", id);
|
||||
return StatusCode(500, "Internal server error while retrieving user");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the current user's profile
|
||||
/// </summary>
|
||||
/// <returns>Current user's profile</returns>
|
||||
[HttpGet("me")]
|
||||
public async Task<ActionResult<UserDto>> GetCurrentUser()
|
||||
{
|
||||
try
|
||||
{
|
||||
var userIdClaim = User.FindFirst("user_id")?.Value;
|
||||
if (string.IsNullOrEmpty(userIdClaim) || !int.TryParse(userIdClaim, out var userId))
|
||||
{
|
||||
return Unauthorized("Invalid user token");
|
||||
}
|
||||
|
||||
return await GetUser(userId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error retrieving current user profile");
|
||||
return StatusCode(500, "Internal server error while retrieving current user");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new user
|
||||
/// </summary>
|
||||
/// <param name="request">User creation request</param>
|
||||
/// <returns>Created user</returns>
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<UserDto>> CreateUser([FromBody] CreateUserRequest request)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return BadRequest(ModelState);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Check if user with this email already exists
|
||||
var existingUser = await _context.Users
|
||||
.FirstOrDefaultAsync(u => u.Email == request.Email);
|
||||
|
||||
if (existingUser != null)
|
||||
{
|
||||
return Conflict($"User with email {request.Email} already exists");
|
||||
}
|
||||
|
||||
var user = new User
|
||||
{
|
||||
Email = request.Email,
|
||||
FirstName = request.FirstName,
|
||||
LastName = request.LastName,
|
||||
ProfilePictureUrl = request.ProfilePictureUrl,
|
||||
IsActive = request.IsActive,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_context.Users.Add(user);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("Created new user with ID {UserId} and email {Email}", user.Id, user.Email);
|
||||
|
||||
var userDto = new UserDto
|
||||
{
|
||||
Id = user.Id,
|
||||
Email = user.Email,
|
||||
FirstName = user.FirstName,
|
||||
LastName = user.LastName,
|
||||
ProfilePictureUrl = user.ProfilePictureUrl,
|
||||
CreatedAt = user.CreatedAt,
|
||||
LastLoginAt = user.LastLoginAt,
|
||||
IsActive = user.IsActive,
|
||||
OAuthProviders = new List<UserOAuthProviderDto>()
|
||||
};
|
||||
|
||||
return CreatedAtAction(nameof(GetUser), new { id = user.Id }, userDto);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error creating user with email {Email}", request.Email);
|
||||
return StatusCode(500, "Internal server error while creating user");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update a user
|
||||
/// </summary>
|
||||
/// <param name="id">User ID</param>
|
||||
/// <param name="request">User update request</param>
|
||||
/// <returns>No content on success</returns>
|
||||
[HttpPut("{id}")]
|
||||
public async Task<IActionResult> UpdateUser(int id, [FromBody] UpdateUserRequest request)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return BadRequest(ModelState);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var user = await _context.Users.FindAsync(id);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"User with ID {id} not found");
|
||||
}
|
||||
|
||||
// Update only provided fields
|
||||
if (request.FirstName != null)
|
||||
{
|
||||
user.FirstName = request.FirstName;
|
||||
}
|
||||
|
||||
if (request.LastName != null)
|
||||
{
|
||||
user.LastName = request.LastName;
|
||||
}
|
||||
|
||||
if (request.ProfilePictureUrl != null)
|
||||
{
|
||||
user.ProfilePictureUrl = request.ProfilePictureUrl;
|
||||
}
|
||||
|
||||
if (request.IsActive.HasValue)
|
||||
{
|
||||
user.IsActive = request.IsActive.Value;
|
||||
}
|
||||
|
||||
_context.Entry(user).State = EntityState.Modified;
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("Updated user with ID {UserId}", id);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
catch (DbUpdateConcurrencyException)
|
||||
{
|
||||
if (!await _context.Users.AnyAsync(e => e.Id == id))
|
||||
{
|
||||
return NotFound($"User with ID {id} not found");
|
||||
}
|
||||
else
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error updating user with ID {UserId}", id);
|
||||
return StatusCode(500, "Internal server error while updating user");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the current user's profile
|
||||
/// </summary>
|
||||
/// <param name="request">User update request</param>
|
||||
/// <returns>No content on success</returns>
|
||||
[HttpPut("me")]
|
||||
public async Task<IActionResult> UpdateCurrentUser([FromBody] UpdateUserRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var userIdClaim = User.FindFirst("user_id")?.Value;
|
||||
if (string.IsNullOrEmpty(userIdClaim) || !int.TryParse(userIdClaim, out var userId))
|
||||
{
|
||||
return Unauthorized("Invalid user token");
|
||||
}
|
||||
|
||||
return await UpdateUser(userId, request);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error updating current user profile");
|
||||
return StatusCode(500, "Internal server error while updating current user");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete a user (soft delete by setting IsActive to false)
|
||||
/// </summary>
|
||||
/// <param name="id">User ID</param>
|
||||
/// <returns>No content on success</returns>
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<IActionResult> DeleteUser(int id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var user = await _context.Users.FindAsync(id);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"User with ID {id} not found");
|
||||
}
|
||||
|
||||
// Soft delete by setting IsActive to false
|
||||
user.IsActive = false;
|
||||
_context.Entry(user).State = EntityState.Modified;
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("Soft deleted user with ID {UserId}", id);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error deleting user with ID {UserId}", id);
|
||||
return StatusCode(500, "Internal server error while deleting user");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Permanently delete a user and all associated OAuth providers
|
||||
/// </summary>
|
||||
/// <param name="id">User ID</param>
|
||||
/// <returns>No content on success</returns>
|
||||
[HttpDelete("{id}/permanent")]
|
||||
public async Task<IActionResult> PermanentlyDeleteUser(int id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var user = await _context.Users
|
||||
.Include(u => u.OAuthProviders)
|
||||
.FirstOrDefaultAsync(u => u.Id == id);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"User with ID {id} not found");
|
||||
}
|
||||
|
||||
// Remove all OAuth providers first due to foreign key constraints
|
||||
_context.UserOAuthProviders.RemoveRange(user.OAuthProviders);
|
||||
|
||||
// Remove the user
|
||||
_context.Users.Remove(user);
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("Permanently deleted user with ID {UserId} and {ProviderCount} OAuth providers",
|
||||
id, user.OAuthProviders.Count);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error permanently deleting user with ID {UserId}", id);
|
||||
return StatusCode(500, "Internal server error while permanently deleting user");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reactivate a soft-deleted user
|
||||
/// </summary>
|
||||
/// <param name="id">User ID</param>
|
||||
/// <returns>No content on success</returns>
|
||||
[HttpPost("{id}/reactivate")]
|
||||
public async Task<IActionResult> ReactivateUser(int id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var user = await _context.Users.FindAsync(id);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"User with ID {id} not found");
|
||||
}
|
||||
|
||||
user.IsActive = true;
|
||||
_context.Entry(user).State = EntityState.Modified;
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("Reactivated user with ID {UserId}", id);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error reactivating user with ID {UserId}", id);
|
||||
return StatusCode(500, "Internal server error while reactivating user");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get user statistics
|
||||
/// </summary>
|
||||
/// <returns>User statistics</returns>
|
||||
[HttpGet("statistics")]
|
||||
public async Task<ActionResult<object>> GetUserStatistics()
|
||||
{
|
||||
try
|
||||
{
|
||||
var totalUsers = await _context.Users.CountAsync();
|
||||
var activeUsers = await _context.Users.CountAsync(u => u.IsActive);
|
||||
var inactiveUsers = totalUsers - activeUsers;
|
||||
var usersWithLogin = await _context.Users.CountAsync(u => u.LastLoginAt != null);
|
||||
var recentUsers = await _context.Users.CountAsync(u => u.CreatedAt >= DateTime.UtcNow.AddDays(-30));
|
||||
|
||||
var providerStats = await _context.UserOAuthProviders
|
||||
.GroupBy(op => op.Provider)
|
||||
.Select(g => new
|
||||
{
|
||||
Provider = g.Key.ToString(),
|
||||
Count = g.Count(),
|
||||
UniqueUsers = g.Select(op => op.UserId).Distinct().Count()
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
totalUsers,
|
||||
activeUsers,
|
||||
inactiveUsers,
|
||||
usersWithLogin,
|
||||
recentUsers,
|
||||
providerStatistics = providerStats
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error retrieving user statistics");
|
||||
return StatusCode(500, "Internal server error while retrieving statistics");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user