diff --git a/EnvelopeGenerator.Application/Common/Dto/UserPreferencesDto.cs b/EnvelopeGenerator.Application/Common/Dto/UserPreferencesDto.cs new file mode 100644 index 00000000..ffc794f6 --- /dev/null +++ b/EnvelopeGenerator.Application/Common/Dto/UserPreferencesDto.cs @@ -0,0 +1,26 @@ +namespace EnvelopeGenerator.Application.Common.Dto; + +/// +/// Represents a user's UI preferences stored in the distributed cache (dbo.TBDD_CACHE). +/// +public sealed class UserPreferencesDto +{ + /// + /// The active DevExpress theme name. + /// Allowed values: "blazing-berry", "fluent", "purple" + /// + public string ThemeName { get; set; } = "blazing-berry"; + + /// + /// Whether dark mode is active. + /// Default: false (light mode). + /// + public bool IsDarkMode { get; set; } = false; + + /// + /// 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(). + /// + public Dictionary GridLayouts { get; set; } = new(); +} diff --git a/EnvelopeGenerator.Application/Common/Interfaces/Services/IUserPreferencesService.cs b/EnvelopeGenerator.Application/Common/Interfaces/Services/IUserPreferencesService.cs new file mode 100644 index 00000000..015e6670 --- /dev/null +++ b/EnvelopeGenerator.Application/Common/Interfaces/Services/IUserPreferencesService.cs @@ -0,0 +1,29 @@ +using EnvelopeGenerator.Application.Common.Dto; + +namespace EnvelopeGenerator.Application.Common.Interfaces.Services; + +/// +/// 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}" +/// +public interface IUserPreferencesService +{ + /// + /// Retrieves preferences for the given user. + /// Always returns a valid object — never throws, never returns null. + /// Returns defaults if no entry exists in cache. + /// + /// The sender's username (ClaimTypes.Name from JWT). + /// Cancellation token. + Task GetAsync(string username, CancellationToken ct = default); + + /// + /// Persists the given preferences for the user in the distributed cache. + /// Failures are non-fatal and silently ignored. + /// + /// The sender's username (ClaimTypes.Name from JWT). + /// The preferences to persist. + /// Cancellation token. + Task SaveAsync(string username, UserPreferencesDto preferences, CancellationToken ct = default); +} diff --git a/EnvelopeGenerator.Infrastructure/DependencyInjection.cs b/EnvelopeGenerator.Infrastructure/DependencyInjection.cs index 00cf5aba..560aa938 100644 --- a/EnvelopeGenerator.Infrastructure/DependencyInjection.cs +++ b/EnvelopeGenerator.Infrastructure/DependencyInjection.cs @@ -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(); services.TryAddScoped(); services.TryAddScoped(); + + // User Preferences — distributed cache (dbo.TBDD_CACHE) + services.TryAddScoped(); #endif services.AddDbRepository(opt => diff --git a/EnvelopeGenerator.Infrastructure/EnvelopeGenerator.Infrastructure.csproj b/EnvelopeGenerator.Infrastructure/EnvelopeGenerator.Infrastructure.csproj index 23ff4950..87dfe75f 100644 --- a/EnvelopeGenerator.Infrastructure/EnvelopeGenerator.Infrastructure.csproj +++ b/EnvelopeGenerator.Infrastructure/EnvelopeGenerator.Infrastructure.csproj @@ -26,6 +26,7 @@ + diff --git a/EnvelopeGenerator.Infrastructure/UserPreferencesService.cs b/EnvelopeGenerator.Infrastructure/UserPreferencesService.cs new file mode 100644 index 00000000..a8443a8c --- /dev/null +++ b/EnvelopeGenerator.Infrastructure/UserPreferencesService.cs @@ -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; + +/// +/// 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. +/// +public sealed class UserPreferencesService(IDistributedCache cache) : IUserPreferencesService +{ + private const string KeyPrefix = "signflow.user-preferences:"; + + private static string BuildKey(string username) => $"{KeyPrefix}{username}"; + + /// + public async Task GetAsync(string username, CancellationToken ct = default) + { + var json = await cache.GetStringAsync(BuildKey(username), ct); + + if (json is null) + return new UserPreferencesDto(); + + return JsonSerializer.Deserialize(json) ?? new UserPreferencesDto(); + } + + /// + 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 diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/UserPreferencesController.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/UserPreferencesController.cs new file mode 100644 index 00000000..47f84b27 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/UserPreferencesController.cs @@ -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; + +/// +/// Manages per-user UI preferences (theme, dark mode, grid layouts). +/// Preferences are stored in the distributed cache (dbo.TBDD_CACHE). +/// +[ApiController] +[Route("api/[controller]")] +[Authorize(Policy = AuthPolicy.Sender)] +public class UserPreferencesController(IUserPreferencesService preferencesService) : ControllerBase +{ + private static readonly HashSet AllowedThemes = + ["blazing-berry", "fluent", "purple"]; + + /// + /// Returns the current user's UI preferences. + /// Always returns 200 with defaults if no preferences have been saved yet. + /// + [HttpGet] + public async Task> Get(CancellationToken ct) + { + var username = User.GetUsername(); + var prefs = await preferencesService.GetAsync(username, ct); + return Ok(prefs); + } + + /// + /// Saves (full replace) the current user's UI preferences. + /// + [HttpPut] + public async Task> 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); + } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Program.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Program.cs index 0a0769c6..c0fa7a2e 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Program.cs +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Program.cs @@ -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(); builder.Services.AddScoped();