using System.Text.Json; using Microsoft.EntityFrameworkCore; using MiGu.Server.Auth; using MiGu.Server.Persistence; namespace MiGu.Server.Dashboard; public sealed class DashboardShortcutService { private readonly PlatformDbContext _db; private readonly RbacStore _rbac; private static readonly JsonSerializerOptions JsonOpts = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; public DashboardShortcutService(PlatformDbContext db, RbacStore rbac) { _db = db; _rbac = rbac; } public sealed record QuickEntriesResult(IReadOnlyList Keys, bool UsingDefaults); public async Task GetAsync(string userId, string scope, CancellationToken ct = default) { scope = NormalizeScope(scope); var allowed = AllowedPages(userId, scope); var row = await _db.UserDashboardShortcuts .AsNoTracking() .FirstOrDefaultAsync(x => x.UserId == userId && x.Scope == scope, ct); if (row == null) { var defaults = EnsureMandatoryKeys( FilterKeys(DashboardShortcutCatalog.DefaultKeysForScope(scope), scope, allowed), scope, allowed); return new QuickEntriesResult( defaults.Take(DashboardShortcutCatalog.MaxKeysPerUser).ToList(), UsingDefaults: true); } var keys = ParseKeys(row.KeysJson); var filtered = EnsureMandatoryKeys( FilterKeys(keys, scope, allowed), scope, allowed) .Take(DashboardShortcutCatalog.MaxKeysPerUser) .ToList(); return new QuickEntriesResult(filtered, UsingDefaults: false); } public async Task SaveAsync( string userId, string scope, IReadOnlyList? keys, CancellationToken ct = default) { scope = NormalizeScope(scope); var allowed = AllowedPages(userId, scope); var sanitized = EnsureMandatoryKeys( FilterKeys(Deduplicate(keys ?? []), scope, allowed), scope, allowed); if (sanitized.Count > DashboardShortcutCatalog.MaxKeysPerUser) sanitized = sanitized.Take(DashboardShortcutCatalog.MaxKeysPerUser).ToList(); var row = await _db.UserDashboardShortcuts .FirstOrDefaultAsync(x => x.UserId == userId && x.Scope == scope, ct); var json = JsonSerializer.Serialize(sanitized, JsonOpts); var now = DateTimeOffset.UtcNow; if (row == null) { _db.UserDashboardShortcuts.Add(new UserDashboardShortcut { UserId = userId, Scope = scope, KeysJson = json, UpdatedAt = now }); } else { row.KeysJson = json; row.UpdatedAt = now; } await _db.SaveChangesAsync(ct); return new QuickEntriesResult(sanitized, UsingDefaults: false); } private HashSet AllowedPages(string userId, string scope) { var user = _rbac.FindUserById(userId); if (user == null || !user.Enabled) return new HashSet(StringComparer.OrdinalIgnoreCase); var eff = _rbac.ComputeEffective(user, scope); return eff.Pages.ToHashSet(StringComparer.OrdinalIgnoreCase); } private static List FilterKeys( IEnumerable keys, string scope, HashSet allowedPages) { var outKeys = new List(); var seen = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var raw in keys) { var key = DashboardShortcutCatalog.NormalizeKey(raw ?? ""); if (string.IsNullOrEmpty(key)) continue; if (!DashboardShortcutCatalog.IsValidKey(key)) continue; if (!DashboardShortcutCatalog.KeyMatchesScope(key, scope)) continue; if (seen.Contains(key)) continue; var pageKey = DashboardShortcutCatalog.PageKeyFor(key); if (!allowedPages.Contains(pageKey)) continue; seen.Add(key); outKeys.Add(key); } return outKeys; } private static List EnsureMandatoryKeys( List keys, string scope, HashSet allowedPages) { if (!string.Equals(scope, PageCatalog.ScopePlatform, StringComparison.OrdinalIgnoreCase)) return keys; var result = new List(); var seen = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var raw in DashboardShortcutCatalog.MandatoryPlatformKeys) { var key = DashboardShortcutCatalog.NormalizeKey(raw); if (!DashboardShortcutCatalog.IsValidKey(key)) continue; var pageKey = DashboardShortcutCatalog.PageKeyFor(key); if (!allowedPages.Contains(pageKey)) continue; if (seen.Add(key)) result.Add(key); } foreach (var key in keys) { if (seen.Add(key)) result.Add(key); } return result; } private static List Deduplicate(IReadOnlyList keys) { var seen = new HashSet(StringComparer.OrdinalIgnoreCase); var list = new List(); foreach (var k in keys) { if (string.IsNullOrWhiteSpace(k)) continue; var t = k.Trim(); if (seen.Add(t)) list.Add(t); } return list; } private static List ParseKeys(string json) { try { return JsonSerializer.Deserialize>(json, JsonOpts) ?? []; } catch { return []; } } private static string NormalizeScope(string scope) => string.Equals(scope, PageCatalog.ScopeMonitor, StringComparison.OrdinalIgnoreCase) ? PageCatalog.ScopeMonitor : PageCatalog.ScopePlatform; }