SF-74: Server — UserPreferencesController, claim extensions, DI registration, App.razor

This commit is contained in:
2026-09-03 14:57:44 +02:00
parent a9ff0d88f2
commit 6effb8a0d6
4 changed files with 42 additions and 7 deletions

View File

@@ -17,6 +17,7 @@
<script src="_content/DevExpress.Blazor.Resources/js/preload-script.js"></script> <script src="_content/DevExpress.Blazor.Resources/js/preload-script.js"></script>
<script src="js/typed.umd.js"></script> <script src="js/typed.umd.js"></script>
<script src="js/receiver-signature.js?v=9"></script> <script src="js/receiver-signature.js?v=9"></script>
<script src="js/signflow-theme.js"></script>
<script src="_framework/blazor.web.js"></script> <script src="_framework/blazor.web.js"></script>
</body> </body>

View File

@@ -10,24 +10,37 @@ namespace EnvelopeGenerator.Server.Controllers;
/// <summary> /// <summary>
/// Manages per-user UI preferences (theme, dark mode, grid layouts). /// Manages per-user UI preferences (theme, dark mode, grid layouts).
/// Preferences are stored in the distributed cache (dbo.TBDD_CACHE). /// Preferences are stored in the distributed cache (dbo.TBDD_CACHE).
/// Accessible by both Sender and Receiver roles.
/// </summary> /// </summary>
[ApiController] [ApiController]
[Route("api/[controller]")] [Route("api/[controller]")]
[Authorize(Policy = AuthPolicy.Sender)] [Authorize(Policy = AuthPolicy.SenderOrReceiver)]
public class UserPreferencesController(IUserPreferencesService preferencesService) : ControllerBase public class UserPreferencesController(IUserPreferencesService preferencesService) : ControllerBase
{ {
private static readonly HashSet<string> AllowedThemes = private static readonly HashSet<string> AllowedThemes =
["blazing-berry", "fluent", "purple"]; ["blazing-berry", "blazing-dark", "purple"];
/// <summary> /// <summary>
/// Returns the current user's UI preferences. /// Returns the current user's UI preferences.
/// Always returns 200 with defaults if no preferences have been saved yet. /// Always returns 200 with defaults if no preferences have been saved yet.
/// DisplayName is populated from JWT claims for senders only.
/// </summary> /// </summary>
[HttpGet] [HttpGet]
public async Task<ActionResult<UserPreferencesDto>> Get(CancellationToken ct) public async Task<ActionResult<UserPreferencesDto>> Get(CancellationToken ct)
{ {
var username = User.GetUsername(); bool isReceiver = User.IsReceiver();
var prefs = await preferencesService.GetAsync(username, ct);
var userId = isReceiver ? User.ReceiverMail() : User.GetUsername();
if (string.IsNullOrWhiteSpace(userId))
return Unauthorized();
var prefs = await preferencesService.GetOrCreateAsync(userId, ct);
// Populate DisplayName for sender only (receiver has no given/surname claims)
if (!isReceiver)
prefs.DisplayName = $"{User.GetPrename()} {User.GetName()}".Trim();
return Ok(prefs); return Ok(prefs);
} }
@@ -42,8 +55,12 @@ public class UserPreferencesController(IUserPreferencesService preferencesServic
if (!AllowedThemes.Contains(dto.ThemeName)) if (!AllowedThemes.Contains(dto.ThemeName))
return BadRequest($"Invalid theme '{dto.ThemeName}'. Allowed: {string.Join(", ", AllowedThemes)}."); return BadRequest($"Invalid theme '{dto.ThemeName}'. Allowed: {string.Join(", ", AllowedThemes)}.");
var username = User.GetUsername(); var userId = User.IsReceiver() ? User.ReceiverMail() : User.GetUsername();
await preferencesService.SaveAsync(username, dto, ct);
if (string.IsNullOrWhiteSpace(userId))
return Unauthorized();
await preferencesService.SaveAsync(userId, dto, ct);
return Ok(dto); return Ok(dto);
} }
} }

View File

@@ -1,4 +1,6 @@
using DigitalData.Auth.Claims; using DigitalData.Auth.Claims;
using DigitalData.UserManager.Domain.Entities;
using EnvelopeGenerator.Domain.Constants;
using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.JsonWebTokens;
using System.Security.Claims; using System.Security.Claims;
@@ -93,4 +95,14 @@ public static class ReceiverClaimExtensions
else else
throw new InvalidOperationException($"Claim '{EnvelopeClaimNames.ReceiverId}' is not a valid integer."); throw new InvalidOperationException($"Claim '{EnvelopeClaimNames.ReceiverId}' is not a valid integer.");
} }
/// <summary>
///
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
public static bool IsReceiver(this ClaimsPrincipal user)
{
return Role.Receiver.Authorized.Any(user.IsInRole);
}
} }

View File

@@ -271,7 +271,12 @@ try
.AddAuthenticationSchemes(AuthScheme.Receiver) .AddAuthenticationSchemes(AuthScheme.Receiver)
.RequireAuthenticatedUser() .RequireAuthenticatedUser()
.RequireRole(Role.Receiver.Full, "receiver")) .RequireRole(Role.Receiver.Full, "receiver"))
.AddPolicy(AuthPolicy.ReceiverTFA, policy => policy.RequireRole(Role.Receiver.TFA)); .AddPolicy(AuthPolicy.ReceiverTFA, policy => policy.RequireRole(Role.Receiver.TFA))
// Combined policy: accepts both Sender and Receiver tokens.
// Used by endpoints accessible to both roles (e.g. UserPreferences).
.AddPolicy(AuthPolicy.SenderOrReceiver, policy => policy
.AddAuthenticationSchemes(AuthScheme.Sender, AuthScheme.Receiver)
.RequireAuthenticatedUser());
// User Manager // User Manager
#pragma warning disable CS0618 #pragma warning disable CS0618