Add user preferences management via distributed cache
Introduced `UserPreferencesDto` to represent UI preferences, including theme, dark mode, and grid layouts. Added the `IUserPreferencesService` interface and implemented it in `UserPreferencesService` to manage preferences stored in the distributed cache (`dbo.TBDD_CACHE`). Registered `UserPreferencesService` in dependency injection and added a NuGet reference for `Microsoft.Extensions.Caching.Abstractions`. Created `UserPreferencesController` with `GET` and `PUT` endpoints to retrieve and save user preferences. Validated theme names in the `PUT` endpoint. Updated `Program.cs` to ensure proper service registration.
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a user's UI preferences stored in the distributed cache (dbo.TBDD_CACHE).
|
||||
/// </summary>
|
||||
public sealed class UserPreferencesDto
|
||||
{
|
||||
/// <summary>
|
||||
/// The active DevExpress theme name.
|
||||
/// Allowed values: "blazing-berry", "fluent", "purple"
|
||||
/// </summary>
|
||||
public string ThemeName { get; set; } = "blazing-berry";
|
||||
|
||||
/// <summary>
|
||||
/// Whether dark mode is active.
|
||||
/// Default: false (light mode).
|
||||
/// </summary>
|
||||
public bool IsDarkMode { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Persisted DxGrid layout states, keyed by a stable grid identifier.
|
||||
/// Example keys: "sender.active-envelopes", "sender.completed-envelopes"
|
||||
/// Values are opaque JSON strings produced by DxGrid.SaveLayoutAsync().
|
||||
/// </summary>
|
||||
public Dictionary<string, string> GridLayouts { get; set; } = new();
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Manages per-user UI preferences (theme, dark mode, grid layouts).
|
||||
/// Preferences are stored in the distributed cache (dbo.TBDD_CACHE) as JSON.
|
||||
/// Cache key schema: "signflow.user-preferences:{username}"
|
||||
/// </summary>
|
||||
public interface IUserPreferencesService
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves preferences for the given user.
|
||||
/// Always returns a valid object — never throws, never returns null.
|
||||
/// Returns defaults if no entry exists in cache.
|
||||
/// </summary>
|
||||
/// <param name="username">The sender's username (ClaimTypes.Name from JWT).</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
Task<UserPreferencesDto> GetAsync(string username, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Persists the given preferences for the user in the distributed cache.
|
||||
/// Failures are non-fatal and silently ignored.
|
||||
/// </summary>
|
||||
/// <param name="username">The sender's username (ClaimTypes.Name from JWT).</param>
|
||||
/// <param name="preferences">The preferences to persist.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
Task SaveAsync(string username, UserPreferencesDto preferences, CancellationToken ct = default);
|
||||
}
|
||||
@@ -14,6 +14,7 @@ using EnvelopeGenerator.Infrastructure.Repositories;
|
||||
using EnvelopeGenerator.Infrastructure.Executor;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.Repositories;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
|
||||
#elif NETFRAMEWORK
|
||||
using System;
|
||||
@@ -55,6 +56,9 @@ namespace EnvelopeGenerator.Infrastructure
|
||||
services.TryAddScoped<IEnvelopeTypeRepository, EnvelopeTypeRepository>();
|
||||
services.TryAddScoped<IReceiverRepository, ReceiverRepository>();
|
||||
services.TryAddScoped<IEnvelopeReceiverReadOnlyRepository, EnvelopeReceiverReadOnlyRepository>();
|
||||
|
||||
// User Preferences — distributed cache (dbo.TBDD_CACHE)
|
||||
services.TryAddScoped<IUserPreferencesService, UserPreferencesService>();
|
||||
#endif
|
||||
|
||||
services.AddDbRepository(opt =>
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
<PackageReference Include="DigitalData.Core.Infrastructure" Version="2.6.1" />
|
||||
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.6" />
|
||||
<PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" />
|
||||
<PackageReference Include="QuestPDF" Version="2025.7.1" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
||||
|
||||
41
EnvelopeGenerator.Infrastructure/UserPreferencesService.cs
Normal file
41
EnvelopeGenerator.Infrastructure/UserPreferencesService.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
#if NET
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||
|
||||
namespace EnvelopeGenerator.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// Implements user preferences storage using the distributed cache (dbo.TBDD_CACHE).
|
||||
/// No DB schema changes required — uses the existing SQL Server distributed cache table.
|
||||
/// Cache key schema: "signflow.user-preferences:{username}"
|
||||
/// Expiration: indefinite (no TTL) — preferences persist until explicitly overwritten.
|
||||
/// </summary>
|
||||
public sealed class UserPreferencesService(IDistributedCache cache) : IUserPreferencesService
|
||||
{
|
||||
private const string KeyPrefix = "signflow.user-preferences:";
|
||||
|
||||
private static string BuildKey(string username) => $"{KeyPrefix}{username}";
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<UserPreferencesDto> GetAsync(string username, CancellationToken ct = default)
|
||||
{
|
||||
var json = await cache.GetStringAsync(BuildKey(username), ct);
|
||||
|
||||
if (json is null)
|
||||
return new UserPreferencesDto();
|
||||
|
||||
return JsonSerializer.Deserialize<UserPreferencesDto>(json) ?? new UserPreferencesDto();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task SaveAsync(string username, UserPreferencesDto preferences, CancellationToken ct = default)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(preferences);
|
||||
|
||||
// No expiration — preferences are stored indefinitely.
|
||||
await cache.SetStringAsync(BuildKey(username), json, new DistributedCacheEntryOptions(), ct);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,49 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.Server.Extensions;
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||
|
||||
namespace EnvelopeGenerator.Server.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Manages per-user UI preferences (theme, dark mode, grid layouts).
|
||||
/// Preferences are stored in the distributed cache (dbo.TBDD_CACHE).
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
[Authorize(Policy = AuthPolicy.Sender)]
|
||||
public class UserPreferencesController(IUserPreferencesService preferencesService) : ControllerBase
|
||||
{
|
||||
private static readonly HashSet<string> AllowedThemes =
|
||||
["blazing-berry", "fluent", "purple"];
|
||||
|
||||
/// <summary>
|
||||
/// Returns the current user's UI preferences.
|
||||
/// Always returns 200 with defaults if no preferences have been saved yet.
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<UserPreferencesDto>> Get(CancellationToken ct)
|
||||
{
|
||||
var username = User.GetUsername();
|
||||
var prefs = await preferencesService.GetAsync(username, ct);
|
||||
return Ok(prefs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves (full replace) the current user's UI preferences.
|
||||
/// </summary>
|
||||
[HttpPut]
|
||||
public async Task<ActionResult<UserPreferencesDto>> Put(
|
||||
[FromBody] UserPreferencesDto dto,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!AllowedThemes.Contains(dto.ThemeName))
|
||||
return BadRequest($"Invalid theme '{dto.ThemeName}'. Allowed: {string.Join(", ", AllowedThemes)}.");
|
||||
|
||||
var username = User.GetUsername();
|
||||
await preferencesService.SaveAsync(username, dto, ct);
|
||||
return Ok(dto);
|
||||
}
|
||||
}
|
||||
@@ -318,6 +318,9 @@ try
|
||||
.AddEnvelopeGeneratorServices(config);
|
||||
#pragma warning restore CS0618
|
||||
|
||||
// User Preferences (theme, dark mode, grid layouts — stored in TBDD_CACHE)
|
||||
// Registration handled by Infrastructure DependencyInjection (AddEnvelopeGeneratorInfrastructureServices)
|
||||
|
||||
// Business Services (Server specific)
|
||||
builder.Services.AddScoped<DocumentService>();
|
||||
builder.Services.AddScoped<AuthService>();
|
||||
|
||||
Reference in New Issue
Block a user