Add admin user management and password-change flow

Introduce full admin user listing/detail endpoints and a forced password-change flow. Backend: make CurrentUserResponse.UserName nullable and add ToCurrentUserResponseAsync extension; AppUserController now exposes GET /auth/user (list) and GET /auth/user/{id} (detail) using UserManager and Admin-only policy; AuthController uses the new mapper and after successful password change clears MustChangePassword, updates UpdatedAt and persists changes (with error handling) before updating security stamp. Frontend: add admin users pages (list + detail), ChangePassword page and route, adminUsers and enhanced authSession services (typed responses, changePassword API, error mapping), router guard to redirect users with mustChangePassword=true to the change-password flow, and show success banner on login after password change. UI tweaks: separate admin section in sidebar, add password-change entries in account menu, footer sizing fixes, and various layout/UX improvements. These changes enable admin account management and enforce secure password updates across the app.
This commit is contained in:
Jonas
2026-04-20 21:02:16 +02:00
parent b2984fcf1a
commit 14176a3ee2
14 changed files with 995 additions and 92 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
public class CurrentUserResponse
{
public Guid Id { get; set; }
public string UserName { get; set; } = string.Empty;
public string? UserName { get; set; } = string.Empty;
public List<string> Roles { get; set; } = new();
public bool IsActive { get; set; }
public bool MustChangePassword { get; set; }
@@ -0,0 +1,27 @@
using API.Models;
using Microsoft.AspNetCore.Identity;
namespace API.Contracts.Auth
{
public static class CurrentUserResponseExtensions
{
public static async Task<CurrentUserResponse> ToCurrentUserResponseAsync(
this AppUser user,
UserManager<AppUser> userManager)
{
ArgumentNullException.ThrowIfNull(user);
ArgumentNullException.ThrowIfNull(userManager);
var roles = await userManager.GetRolesAsync(user);
return new CurrentUserResponse
{
Id = user.Id,
UserName = user.UserName,
Roles = roles.OrderBy(role => role).ToList(),
IsActive = user.IsActive,
MustChangePassword = user.MustChangePassword,
};
}
}
}
+25 -4
View File
@@ -1,18 +1,39 @@
using API.Contracts.Auth;
using API.Models;
using API.Security;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace API.Controllers.Auth
{
[ApiController]
[Authorize(Policy = PolicyNames.AdminOnly)]
[Route("auth/user")]
public class AppUserController : ControllerBase
public class AppUserController(UserManager<AppUser> userManager) : ControllerBase
{
[HttpGet]
[Authorize(Policy = PolicyNames.AdminOnly)]
public IActionResult GetAppUsers()
public async Task<ActionResult<IReadOnlyList<CurrentUserResponse>>> GetAppUsers()
{
return Ok(new { message = "Adminzugriff bestätigt." });
var users = await userManager.Users
.OrderBy(x => x.UserName)
.ToListAsync();
var tasks = users.Select(user => user.ToCurrentUserResponseAsync(userManager));
return Ok(await Task.WhenAll(tasks));
}
[HttpGet("{id:guid}")]
public async Task<ActionResult<CurrentUserResponse>> GetAppUserById([FromRoute] Guid id)
{
var user = await userManager.Users.FirstOrDefaultAsync(x => x.Id == id);
if (user is null)
{
return NotFound(new { message = "Benutzer wurde nicht gefunden." });
}
return Ok(await user.ToCurrentUserResponseAsync(userManager));
}
}
}
+13 -10
View File
@@ -58,16 +58,7 @@ namespace API.Controllers.Auth
if (user is null)
return Unauthorized();
var roles = await userManager.GetRolesAsync(user);
return Ok(new CurrentUserResponse
{
Id = user.Id,
UserName = user.UserName ?? string.Empty,
Roles = roles.OrderBy(x => x).ToList(),
IsActive = user.IsActive,
MustChangePassword = user.MustChangePassword
});
return Ok(await user.ToCurrentUserResponseAsync(userManager));
}
[HttpPost("password")]
@@ -105,6 +96,18 @@ namespace API.Controllers.Auth
});
}
user.MustChangePassword = false;
user.UpdatedAt = DateTimeOffset.UtcNow;
var updateResult = await userManager.UpdateAsync(user);
if (!updateResult.Succeeded)
{
return StatusCode(500, new
{
message = "Passwort wurde geändert, Benutzerdaten konnten aber nicht final gespeichert werden.",
errors = updateResult.Errors.Select(e => e.Description)
});
}
var stampResult = await userManager.UpdateSecurityStampAsync(user);
if (!stampResult.Succeeded)
{