First Commit
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
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<ApplicationUser> _users;
|
||||
private readonly RoleManager<IdentityRole> _roles;
|
||||
private readonly IAppEmailSender _email;
|
||||
private readonly IConfiguration _cfg;
|
||||
public UsersController(UserManager<ApplicationUser> users, RoleManager<IdentityRole> 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<string> Roles);
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<List<UserDto>>> List(CancellationToken cancellationToken)
|
||||
{
|
||||
var items = await _users.Users
|
||||
.OrderBy(u => u.Email)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var outList = new List<UserDto>(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<ActionResult<UserDto>> 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<string>()).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<IActionResult> 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<string>()).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<IActionResult> 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<IActionResult> 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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user