Files
jobtrackingapp/JobTrackerApi/Controllers/UsersController.cs

227 lines
8.4 KiB
C#

using JobTrackerApi.Models;
using JobTrackerApi.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Security.Claims;
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;
private readonly ILogger<UsersController> _logger;
public UsersController(UserManager<ApplicationUser> users, RoleManager<IdentityRole> roles, IAppEmailSender email, IConfiguration cfg, ILogger<UsersController> logger)
{
_users = users;
_roles = roles;
_email = email;
_cfg = cfg;
_logger = logger;
}
public sealed record UserDto(
string Id,
string? Email,
string? UserName,
string? FirstName,
string? LastName,
string? DisplayName,
bool EmailConfirmed,
string? GoogleEmail,
DateTimeOffset? GoogleLinkedAt,
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(ToDto(u, rs.ToList()));
}
return Ok(outList);
}
public sealed record CreateUserRequest(string Email, string Password, string? UserName, string? FirstName, string? LastName, string? DisplayName, 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 = string.IsNullOrWhiteSpace(request.UserName) ? email : request.UserName.Trim(),
Email = email,
EmailConfirmed = true,
FirstName = TrimOrNull(request.FirstName),
LastName = TrimOrNull(request.LastName),
DisplayName = TrimOrNull(request.DisplayName)
};
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(ToDto(u, 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)}";
try
{
await _email.SendAsync(
u.Email,
"Password reset",
$"An admin initiated a password reset for your Jobbjakt account.\n\nReset link:\n{link}\n",
cancellationToken
);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to send admin-initiated password reset email to {Email}", u.Email);
return Problem(statusCode: StatusCodes.Status503ServiceUnavailable, title: "Email delivery unavailable", detail: "Password reset email could not be sent right now. Please try again later.");
}
return NoContent();
}
public sealed record SendTestEmailRequest(string? ToEmail, string? Subject, string? Message);
[HttpPost("send-test-email")]
public async Task<IActionResult> SendTestEmail([FromBody] SendTestEmailRequest? request, CancellationToken cancellationToken)
{
var currentUserId = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub");
var currentUser = currentUserId is null ? null : await _users.FindByIdAsync(currentUserId);
var toEmail = (request?.ToEmail ?? currentUser?.Email ?? "").Trim();
if (string.IsNullOrWhiteSpace(toEmail)) return BadRequest("Recipient email is required.");
var subject = string.IsNullOrWhiteSpace(request?.Subject) ? "Jobbjakt test email" : request!.Subject!.Trim();
var message = string.IsNullOrWhiteSpace(request?.Message)
? "This is a test email from the Jobbjakt admin panel.\n\nIf you received this, the SMTP configuration is working."
: request!.Message!.Trim();
try
{
await _email.SendAsync(
toEmail,
subject,
$"{message}\n\nSent at: {DateTimeOffset.UtcNow:u}",
cancellationToken
);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to send test email to {Email}", toEmail);
return Problem(statusCode: StatusCodes.Status503ServiceUnavailable, title: "Email delivery unavailable", detail: "Test email could not be sent right now. Please try again later.");
}
return NoContent();
}
private static UserDto ToDto(ApplicationUser user, List<string> roles)
{
return new UserDto(
user.Id,
user.Email,
user.UserName,
user.FirstName,
user.LastName,
user.DisplayName,
user.EmailConfirmed,
user.GoogleEmail,
user.GoogleLinkedAt,
roles);
}
private static string? TrimOrNull(string? value)
{
return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
}
}