483 lines
17 KiB
C#
483 lines
17 KiB
C#
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");
|
|
}
|
|
}
|
|
}
|
|
} |