using JobTrackerApi.Models; using JobTrackerApi.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; namespace JobTrackerApi.Controllers; [ApiController] [Route("api/users")] [Authorize(Roles = "Admin")] public sealed class UsersController : ControllerBase { private readonly UserManager _users; private readonly RoleManager _roles; private readonly IAppEmailSender _email; private readonly IConfiguration _cfg; public UsersController(UserManager users, RoleManager roles, IAppEmailSender email, IConfiguration cfg) { _users = users; _roles = roles; _email = email; _cfg = cfg; } public sealed record UserDto(string Id, string? Email, string? UserName, bool EmailConfirmed, List Roles); [HttpGet] public async Task>> List(CancellationToken cancellationToken) { var items = await _users.Users .OrderBy(u => u.Email) .ToListAsync(cancellationToken); var outList = new List(items.Count); foreach (var u in items) { var rs = await _users.GetRolesAsync(u); outList.Add(new UserDto(u.Id, u.Email, u.UserName, u.EmailConfirmed, rs.ToList())); } return Ok(outList); } public sealed record CreateUserRequest(string Email, string Password, string[]? Roles); [HttpPost] public async Task> Create([FromBody] CreateUserRequest request, CancellationToken cancellationToken) { var email = (request.Email ?? "").Trim(); var password = request.Password ?? ""; if (email.Length == 0) return BadRequest("Email is required."); if (password.Length == 0) return BadRequest("Password is required."); var existing = await _users.FindByEmailAsync(email); if (existing is not null) return BadRequest("User already exists."); var u = new ApplicationUser { UserName = email, Email = email, EmailConfirmed = true }; var res = await _users.CreateAsync(u, password); if (!res.Succeeded) return BadRequest(string.Join("; ", res.Errors.Select(e => e.Description))); var roles = (request.Roles ?? Array.Empty()).Select(r => (r ?? "").Trim()).Where(r => r.Length > 0).Distinct(StringComparer.OrdinalIgnoreCase).ToList(); foreach (var r in roles) { if (!await _roles.RoleExistsAsync(r)) await _roles.CreateAsync(new IdentityRole(r)); await _users.AddToRoleAsync(u, r); } var rs = await _users.GetRolesAsync(u); return Ok(new UserDto(u.Id, u.Email, u.UserName, u.EmailConfirmed, rs.ToList())); } public sealed record SetRolesRequest(string[] Roles); [HttpPut("{id}/roles")] public async Task SetRoles([FromRoute] string id, [FromBody] SetRolesRequest request, CancellationToken cancellationToken) { var u = await _users.FindByIdAsync(id); if (u is null) return NotFound(); var desired = (request.Roles ?? Array.Empty()).Select(r => (r ?? "").Trim()).Where(r => r.Length > 0).Distinct(StringComparer.OrdinalIgnoreCase).ToList(); var current = await _users.GetRolesAsync(u); var toRemove = current.Where(r => !desired.Contains(r, StringComparer.OrdinalIgnoreCase)).ToList(); var toAdd = desired.Where(r => !current.Contains(r, StringComparer.OrdinalIgnoreCase)).ToList(); if (toRemove.Count > 0) await _users.RemoveFromRolesAsync(u, toRemove); foreach (var r in toAdd) { if (!await _roles.RoleExistsAsync(r)) await _roles.CreateAsync(new IdentityRole(r)); await _users.AddToRoleAsync(u, r); } return NoContent(); } [HttpDelete("{id}")] public async Task Delete([FromRoute] string id, CancellationToken cancellationToken) { var u = await _users.FindByIdAsync(id); if (u is null) return NotFound(); var res = await _users.DeleteAsync(u); if (!res.Succeeded) return BadRequest(string.Join("; ", res.Errors.Select(e => e.Description))); return NoContent(); } [HttpPost("{id}/send-password-reset")] public async Task SendPasswordReset([FromRoute] string id, CancellationToken cancellationToken) { var u = await _users.FindByIdAsync(id); if (u is null) return NotFound(); if (string.IsNullOrWhiteSpace(u.Email)) return BadRequest("User has no email."); var token = await _users.GeneratePasswordResetTokenAsync(u); var baseUrl = (_cfg["App:PublicBaseUrl"] ?? "").Trim().TrimEnd('/'); if (string.IsNullOrWhiteSpace(baseUrl)) { baseUrl = $"{Request.Scheme}://{Request.Host}"; } var link = $"{baseUrl}/reset-password?email={Uri.EscapeDataString(u.Email)}&token={Uri.EscapeDataString(token)}"; await _email.SendAsync( u.Email, "Password reset", $"An admin initiated a password reset for your Job Tracker account.\n\nReset link:\n{link}\n", cancellationToken ); return NoContent(); } }