Adds profile and settings pages

This commit is contained in:
2026-06-22 23:59:04 +02:00
parent 6976eb4337
commit 2544e20b3c
49 changed files with 2741 additions and 34 deletions
+4
View File
@@ -24,6 +24,10 @@ orchestrator.RegisterModuleServices(builder.Services);
builder.Services.AddControllers(options =>
{
options.Conventions.Add(new ApiPrefixConvention("api/v1"));
})
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter());
});
var app = builder.Build();
@@ -5,6 +5,8 @@ public record LoginRequest(string Email, string Password);
public record InviteUserRequest(string Email, string Role);
public record CompleteSetupRequest(string Token, string Password);
public record ChangeRoleRequest(string NewRole);
public record UpdateProfileRequest(string Name, string Email);
public record ChangePasswordRequest(string CurrentPassword, string NewPassword);
public record SetUserActiveRequest(bool IsActive);
public record PendingInvitationInfo(Guid Id, string Email, string Role, string Token, DateTimeOffset CreatedAt);
public record UserDto(
@@ -77,6 +77,19 @@ public class AuthService : IAuthService
}
}
public async Task ChangePasswordAsync(Guid userId, string currentPassword, string newPassword)
{
var user = await _userManager.FindByIdAsync(userId.ToString())
?? throw new UnauthorizedException("Gebruiker niet gevonden.");
var result = await _userManager.ChangePasswordAsync(user, currentPassword, newPassword);
if (!result.Succeeded)
{
var errors = string.Join(", ", result.Errors.Select(e => e.Description));
throw new ValidationException(errors);
}
}
private async Task<TokenResponse> GenerateTokenResponseAsync(ApplicationUser user)
{
var roles = await _userManager.GetRolesAsync(user);
@@ -21,4 +21,9 @@ public interface IAuthService
/// Trekt een refresh token in (uitloggen).
/// </summary>
Task RevokeTokenAsync(string refreshToken);
/// <summary>
/// Wijzigt het wachtwoord van de opgegeven gebruiker.
/// </summary>
Task ChangePasswordAsync(Guid userId, string currentPassword, string newPassword);
}
@@ -22,10 +22,20 @@ public class AvailabilityMiddleware
_logger = logger;
}
// Paths that are always accessible regardless of system availability.
// Auth and Setup must stay open so admins can log in and the frontend
// can determine whether the system is initialized.
private static readonly string[] _bypassPrefixes =
[
"/api/v1/Availability/status",
"/api/v1/Auth/",
"/api/v1/Setup/status",
];
public async Task InvokeAsync(HttpContext context, IAvailabilityService availabilityService)
{
// Bypass voor status endpoint
if (context.Request.Path.StartsWithSegments("/api/availability/status"))
var path = context.Request.Path.Value ?? string.Empty;
if (_bypassPrefixes.Any(p => path.StartsWith(p, StringComparison.OrdinalIgnoreCase)))
{
await _next(context);
return;
@@ -1,3 +1,4 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@@ -61,6 +62,20 @@ public class AuthController : ControllerBase
return NoContent();
}
[HttpPost("change-password")]
[Authorize]
public async Task<IActionResult> ChangePassword([FromBody] ChangePasswordRequest request)
{
var userIdClaim = User.FindFirstValue(ClaimTypes.NameIdentifier)
?? User.FindFirstValue("sub");
if (!Guid.TryParse(userIdClaim, out var userId))
return Unauthorized();
await _authService.ChangePasswordAsync(userId, request.CurrentPassword, request.NewPassword);
return Ok();
}
private void SetTokenCookie(string refreshToken)
{
Response.Cookies.Append("refreshToken", refreshToken, GetCookieOptions());
@@ -1,3 +1,4 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
@@ -75,6 +76,51 @@ public class UsersController : ControllerBase
return Ok(result.OrderBy(u => u.CreatedAt));
}
[HttpPut("me")]
[Authorize]
public async Task<IActionResult> UpdateProfile([FromBody] UpdateProfileRequest request)
{
var userIdClaim = User.FindFirstValue(ClaimTypes.NameIdentifier)
?? User.FindFirstValue("sub");
if (!Guid.TryParse(userIdClaim, out var userId))
return Unauthorized();
var user = await _userManager.FindByIdAsync(userId.ToString());
if (user is null) return NotFound();
// Check email uniqueness if email is changing
if (!string.Equals(user.Email, request.Email, StringComparison.OrdinalIgnoreCase))
{
var existing = await _userManager.FindByEmailAsync(request.Email);
if (existing is not null)
return BadRequest(new { detail = "Email is already in use." });
}
user.DisplayName = request.Name;
user.Email = request.Email;
user.UserName = request.Email;
user.NormalizedEmail = request.Email.ToUpperInvariant();
user.NormalizedUserName = request.Email.ToUpperInvariant();
var result = await _userManager.UpdateAsync(user);
if (!result.Succeeded)
return StatusCode(500, new { detail = "Failed to update profile." });
var roles = await _userManager.GetRolesAsync(user);
var role = roles.FirstOrDefault() ?? "User";
return Ok(new UserDto(
user.Id,
user.Email ?? string.Empty,
user.DisplayName ?? user.Email ?? string.Empty,
role,
user.IsActive,
user.CreatedAt,
false,
null));
}
[HttpPost("invite")]
public async Task<IActionResult> Invite([FromBody] InviteUserRequest request)
{