Merge branch 'main' of https://git.azuze.fr/kawa/SharepointToolbox-Web
This commit is contained in:
@@ -18,18 +18,21 @@
|
||||
<div class="app-layout">
|
||||
<aside class="sidebar @(_sidebarCollapsed ? "collapsed" : "")">
|
||||
<div class="sidebar-header">
|
||||
<span class="logo-text">SP Toolbox</span>
|
||||
<button class="toggle-btn" @onclick="ToggleSidebar">☰</button>
|
||||
<div class="logo">
|
||||
<span class="logo-mark">SP</span>
|
||||
<span class="logo-text">SP Toolbox</span>
|
||||
</div>
|
||||
<button class="toggle-btn @(_sidebarCollapsed ? "collapsed" : "")" @onclick="ToggleSidebar" title="Toggle sidebar">›</button>
|
||||
</div>
|
||||
|
||||
@* User identity badge *@
|
||||
<AuthorizeView>
|
||||
<Authorized>
|
||||
<div class="profile-badge" style="background:var(--surface-2);border-radius:6px;margin:8px;padding:8px">
|
||||
<div style="font-size:12px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">
|
||||
<div class="identity-badge">
|
||||
<div class="identity-name">
|
||||
@UserContext.DisplayName
|
||||
</div>
|
||||
<div style="font-size:11px;color:var(--text-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">
|
||||
<div class="identity-email">
|
||||
@UserContext.Email
|
||||
</div>
|
||||
<div style="margin-top:4px">
|
||||
@@ -55,80 +58,55 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="nav-search">
|
||||
<span class="nav-icon">🔍</span>
|
||||
<input type="text" class="nav-search-input" placeholder="Search…"
|
||||
@bind="_navFilter" @bind:event="oninput" />
|
||||
@if (!string.IsNullOrEmpty(_navFilter))
|
||||
{
|
||||
<button class="nav-search-clear" @onclick="ClearFilter" title="Clear">✕</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<nav class="nav-menu">
|
||||
<NavLink href="/" Match="NavLinkMatch.All" class="nav-item">
|
||||
<span class="nav-icon">🏠</span><span class="nav-label">Home</span>
|
||||
</NavLink>
|
||||
@if (Session.HasProfile)
|
||||
@{
|
||||
string? lastSection = null;
|
||||
var items = VisibleNavItems().ToList();
|
||||
}
|
||||
@foreach (var item in items)
|
||||
{
|
||||
<NavLink href="/permissions" class="nav-item">
|
||||
<span class="nav-icon">🔐</span><span class="nav-label">Permissions</span>
|
||||
</NavLink>
|
||||
<NavLink href="/storage" class="nav-item">
|
||||
<span class="nav-icon">💾</span><span class="nav-label">Storage</span>
|
||||
</NavLink>
|
||||
<NavLink href="/search" class="nav-item">
|
||||
<span class="nav-icon">🔍</span><span class="nav-label">Search</span>
|
||||
</NavLink>
|
||||
<NavLink href="/duplicates" class="nav-item">
|
||||
<span class="nav-icon">📋</span><span class="nav-label">Duplicates</span>
|
||||
</NavLink>
|
||||
<NavLink href="/versions" class="nav-item">
|
||||
<span class="nav-icon">🗂️</span><span class="nav-label">Version Cleanup</span>
|
||||
</NavLink>
|
||||
<NavLink href="/transfer" class="nav-item">
|
||||
<span class="nav-icon">📦</span><span class="nav-label">File Transfer</span>
|
||||
</NavLink>
|
||||
<div class="nav-divider">Bulk</div>
|
||||
<NavLink href="/bulk-members" class="nav-item">
|
||||
<span class="nav-icon">👥</span><span class="nav-label">Bulk Members</span>
|
||||
</NavLink>
|
||||
<NavLink href="/bulk-sites" class="nav-item">
|
||||
<span class="nav-icon">🌐</span><span class="nav-label">Bulk Sites</span>
|
||||
</NavLink>
|
||||
<NavLink href="/folder-structure" class="nav-item">
|
||||
<span class="nav-icon">📁</span><span class="nav-label">Folder Structure</span>
|
||||
</NavLink>
|
||||
<div class="nav-divider">Audit</div>
|
||||
<NavLink href="/user-audit" class="nav-item">
|
||||
<span class="nav-icon">👤</span><span class="nav-label">User Access Audit</span>
|
||||
</NavLink>
|
||||
<NavLink href="/user-directory" class="nav-item">
|
||||
<span class="nav-icon">📖</span><span class="nav-label">User Directory</span>
|
||||
</NavLink>
|
||||
<div class="nav-divider">Config</div>
|
||||
<NavLink href="/templates" class="nav-item">
|
||||
<span class="nav-icon">📐</span><span class="nav-label">Templates</span>
|
||||
if (item.Section != lastSection)
|
||||
{
|
||||
lastSection = item.Section;
|
||||
if (!string.IsNullOrEmpty(item.Section))
|
||||
{
|
||||
<div class="nav-divider">@item.Section</div>
|
||||
}
|
||||
}
|
||||
<NavLink href="@item.Href" Match="@(item.Href == "/" ? NavLinkMatch.All : NavLinkMatch.Prefix)" class="nav-item">
|
||||
<span class="nav-icon">@item.Icon</span><span class="nav-label">@item.Label</span>
|
||||
</NavLink>
|
||||
}
|
||||
|
||||
@* Admin-only section *@
|
||||
@if (UserContext.Role == UserRole.Admin)
|
||||
@if (items.Count == 0)
|
||||
{
|
||||
<div class="nav-divider">Admin</div>
|
||||
<NavLink href="/profiles" class="nav-item">
|
||||
<span class="nav-icon">⚙️</span><span class="nav-label">Client Profiles</span>
|
||||
</NavLink>
|
||||
<NavLink href="/admin/users" class="nav-item">
|
||||
<span class="nav-icon">👥</span><span class="nav-label">User Management</span>
|
||||
</NavLink>
|
||||
<NavLink href="/admin/audit" class="nav-item">
|
||||
<span class="nav-icon">📋</span><span class="nav-label">Audit Logs</span>
|
||||
</NavLink>
|
||||
<div class="nav-empty">No match</div>
|
||||
}
|
||||
</nav>
|
||||
|
||||
<NavLink href="/settings" class="nav-item">
|
||||
<span class="nav-icon">🔧</span><span class="nav-label">Settings</span>
|
||||
</NavLink>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<AuthorizeView>
|
||||
<Authorized>
|
||||
<a href="/account/logout" class="nav-item" style="color:var(--text-muted)">
|
||||
<a href="/account/logout" class="nav-item">
|
||||
<span class="nav-icon">🚪</span><span class="nav-label">Logout</span>
|
||||
</a>
|
||||
</Authorized>
|
||||
</AuthorizeView>
|
||||
</nav>
|
||||
<button class="nav-item theme-toggle" @onclick="ToggleTheme">
|
||||
<span class="nav-icon">🌙</span>
|
||||
<span class="nav-label">@(_dark ? "Light Mode" : "Dark Mode")</span>
|
||||
<span class="switch @(_dark ? "on" : "")"></span>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="content">
|
||||
@@ -147,7 +125,49 @@
|
||||
|
||||
@code {
|
||||
private bool _sidebarCollapsed;
|
||||
private bool _dark;
|
||||
private string _navFilter = string.Empty;
|
||||
private bool _hasCredentials;
|
||||
|
||||
private sealed record NavItem(string Href, string Icon, string Label, string Section, string Scope);
|
||||
|
||||
private static readonly NavItem[] AllNavItems =
|
||||
{
|
||||
new("/", "🏠", "Home", "", "always"),
|
||||
new("/permissions", "🔐", "Permissions", "", "profile"),
|
||||
new("/storage", "💾", "Storage", "", "profile"),
|
||||
new("/duplicates", "📋", "Duplicates", "", "profile"),
|
||||
new("/versions", "🗂️", "Version Cleanup", "", "profile"),
|
||||
new("/transfer", "📦", "File Transfer", "", "profile"),
|
||||
new("/bulk-members", "👥", "Bulk Members", "Bulk", "profile"),
|
||||
new("/bulk-sites", "🌐", "Bulk Sites", "Bulk", "profile"),
|
||||
new("/folder-structure", "📁", "Folder Structure", "Bulk", "profile"),
|
||||
new("/user-audit", "👤", "User Access Audit","Audit", "profile"),
|
||||
new("/user-directory", "📖", "User Directory", "Audit", "profile"),
|
||||
new("/templates", "📐", "Templates", "Config", "profile"),
|
||||
new("/profiles", "⚙️", "Client Profiles", "Admin", "admin"),
|
||||
new("/admin/users", "👥", "User Management", "Admin", "admin"),
|
||||
new("/admin/audit", "📋", "Audit Logs", "Admin", "admin"),
|
||||
new("/settings", "🔧", "Settings", "", "always"),
|
||||
new("/account/change-password","🔑", "Change Password", "", "auth"),
|
||||
};
|
||||
|
||||
private IEnumerable<NavItem> VisibleNavItems()
|
||||
{
|
||||
var filter = _navFilter?.Trim() ?? string.Empty;
|
||||
return AllNavItems
|
||||
.Where(i => i.Scope switch
|
||||
{
|
||||
"profile" => Session.HasProfile,
|
||||
"admin" => UserContext.Role == UserRole.Admin,
|
||||
"auth" => UserContext.IsAuthenticated,
|
||||
_ => true
|
||||
})
|
||||
.Where(i => filter.Length == 0
|
||||
|| i.Label.Contains(filter, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private void ClearFilter() => _navFilter = string.Empty;
|
||||
private string _credUsername = string.Empty;
|
||||
private SessionCredentialsModal? _credModal;
|
||||
|
||||
@@ -155,6 +175,7 @@
|
||||
{
|
||||
Session.ProfileChanged += OnProfileChanged;
|
||||
UserContext.Initialized += OnUserContextInitialized;
|
||||
_dark = string.Equals(Session.Settings.Theme, "Dark", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private void OnUserContextInitialized() => InvokeAsync(StateHasChanged);
|
||||
@@ -209,7 +230,20 @@
|
||||
|
||||
private async Task RefreshCredentialState()
|
||||
{
|
||||
var tokens = await CredStore.GetAsync();
|
||||
var tokens = await CredStore.GetAsync();
|
||||
|
||||
// Session tokens are tenant-bound (refresh token issued for the profile's TenantId/ClientId).
|
||||
// After switching profiles the stored tokens may belong to the *previous* tenant — treat those
|
||||
// as stale: drop them and the cached CSOM contexts so we don't keep operating against the old
|
||||
// profile, and so the connect prompt below fires for the new one.
|
||||
if (tokens is not null && Session.CurrentProfile is { } profile &&
|
||||
!string.Equals(tokens.TenantId, profile.TenantId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
await CredStore.ClearAsync();
|
||||
await SessionManager.ClearAllAsync();
|
||||
tokens = null;
|
||||
}
|
||||
|
||||
_hasCredentials = tokens is not null && !string.IsNullOrEmpty(tokens.RefreshToken);
|
||||
_credUsername = tokens?.UserPrincipalName ?? string.Empty;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
@@ -234,8 +268,11 @@
|
||||
{
|
||||
InvokeAsync(async () =>
|
||||
{
|
||||
StateHasChanged();
|
||||
// New profile selected → prompt for credentials if none
|
||||
// Re-evaluate credentials against the newly selected profile. This drops tokens
|
||||
// left over from the previous profile (different tenant), so a switch never keeps
|
||||
// operating on the old connection.
|
||||
await RefreshCredentialState();
|
||||
// New profile selected and no valid credentials for it → prompt to connect.
|
||||
if (Session.HasProfile && !_hasCredentials && _credModal is not null)
|
||||
await _credModal.ShowAsync();
|
||||
});
|
||||
@@ -252,6 +289,22 @@
|
||||
|
||||
private void ToggleSidebar() => _sidebarCollapsed = !_sidebarCollapsed;
|
||||
|
||||
private async Task ToggleTheme()
|
||||
{
|
||||
_dark = !_dark;
|
||||
var theme = _dark ? "Dark" : "Light";
|
||||
var s = Session.Settings;
|
||||
Session.UpdateSettings(new AppSettings
|
||||
{
|
||||
DataFolder = s.DataFolder,
|
||||
Lang = s.Lang,
|
||||
AutoTakeOwnership = s.AutoTakeOwnership,
|
||||
Theme = theme,
|
||||
MspLogo = s.MspLogo
|
||||
});
|
||||
await JS.InvokeVoidAsync("sptb.setTheme", theme);
|
||||
}
|
||||
|
||||
private static string RoleChipClass(UserRole role) => role switch
|
||||
{
|
||||
UserRole.Admin => "chip-red",
|
||||
|
||||
Reference in New Issue
Block a user