252 lines
9.8 KiB
Plaintext
252 lines
9.8 KiB
Plaintext
@inherits LayoutComponentBase
|
|
@inject IUserSessionService Session
|
|
@inject IUserContextAccessor UserContext
|
|
@inject ISessionCredentialStore CredStore
|
|
@inject ISessionManager SessionManager
|
|
@inject NavigationManager Nav
|
|
@inject SharepointToolbox.Web.Services.OAuth.IOAuthFlowCache OAuthCache
|
|
@using Microsoft.AspNetCore.Components.Authorization
|
|
@using Microsoft.AspNetCore.WebUtilities
|
|
@using SharepointToolbox.Web.Core.Models
|
|
@using SharepointToolbox.Web.Services.Session
|
|
|
|
<AppInitializer />
|
|
|
|
<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>
|
|
|
|
@* 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">
|
|
@UserContext.DisplayName
|
|
</div>
|
|
<div style="font-size:11px;color:var(--text-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">
|
|
@UserContext.Email
|
|
</div>
|
|
<div style="margin-top:4px">
|
|
<span class="chip @RoleChipClass(UserContext.Role)" style="font-size:10px">@UserContext.Role</span>
|
|
</div>
|
|
@if (_hasCredentials)
|
|
{
|
|
<div style="font-size:10px;color:var(--text-muted);margin-top:4px">
|
|
SP: @_credUsername
|
|
<button class="btn btn-secondary btn-sm" style="padding:2px 6px;font-size:10px;margin-left:4px"
|
|
@onclick="ReconnectAsync">Reconnect</button>
|
|
</div>
|
|
}
|
|
</div>
|
|
</Authorized>
|
|
</AuthorizeView>
|
|
|
|
@if (Session.HasProfile)
|
|
{
|
|
<div class="profile-badge">
|
|
<span class="profile-icon">🏢</span>
|
|
<span class="profile-name">@Session.CurrentProfile!.Name</span>
|
|
</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)
|
|
{
|
|
<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>
|
|
</NavLink>
|
|
}
|
|
|
|
@* Admin-only section *@
|
|
@if (UserContext.Role == UserRole.Admin)
|
|
{
|
|
<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>
|
|
}
|
|
|
|
<NavLink href="/settings" class="nav-item">
|
|
<span class="nav-icon">🔧</span><span class="nav-label">Settings</span>
|
|
</NavLink>
|
|
|
|
<AuthorizeView>
|
|
<Authorized>
|
|
<a href="/account/logout" class="nav-item" style="color:var(--text-muted)">
|
|
<span class="nav-icon">🚪</span><span class="nav-label">Logout</span>
|
|
</a>
|
|
</Authorized>
|
|
</AuthorizeView>
|
|
</nav>
|
|
</aside>
|
|
|
|
<main class="content">
|
|
@if (UserContext.IsAuthenticated)
|
|
{
|
|
@Body
|
|
}
|
|
else
|
|
{
|
|
<div style="padding:2rem;color:var(--text-muted)">Loading…</div>
|
|
}
|
|
</main>
|
|
</div>
|
|
|
|
<SessionCredentialsModal @ref="_credModal" OnConnected="OnCredentialsConnected" />
|
|
|
|
@code {
|
|
private bool _sidebarCollapsed;
|
|
private bool _hasCredentials;
|
|
private string _credUsername = string.Empty;
|
|
private SessionCredentialsModal? _credModal;
|
|
|
|
protected override void OnInitialized()
|
|
{
|
|
Session.ProfileChanged += OnProfileChanged;
|
|
UserContext.Initialized += OnUserContextInitialized;
|
|
}
|
|
|
|
private void OnUserContextInitialized() => InvokeAsync(StateHasChanged);
|
|
|
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
{
|
|
if (!firstRender) return;
|
|
|
|
// Pick up token_key from OAuth callback redirect before checking credential state
|
|
await HandleOAuthCallbackAsync();
|
|
await RefreshCredentialState();
|
|
|
|
// Check for connect_error query param
|
|
var uri = new Uri(Nav.Uri);
|
|
var query = QueryHelpers.ParseQuery(uri.Query);
|
|
if (query.TryGetValue("connect_error", out var err) && !string.IsNullOrEmpty(err))
|
|
{
|
|
Nav.NavigateTo(uri.GetLeftPart(UriPartial.Path), replace: true);
|
|
if (_credModal is not null)
|
|
{
|
|
await _credModal.ShowAsync();
|
|
}
|
|
}
|
|
|
|
// If profile selected but no credentials → show modal
|
|
if (Session.HasProfile && !_hasCredentials && _credModal is not null)
|
|
await _credModal.ShowAsync();
|
|
}
|
|
|
|
private async Task HandleOAuthCallbackAsync()
|
|
{
|
|
var uri = new Uri(Nav.Uri);
|
|
var query = QueryHelpers.ParseQuery(uri.Query);
|
|
|
|
if (!query.TryGetValue("token_key", out var tokenKey) || string.IsNullOrEmpty(tokenKey))
|
|
return;
|
|
|
|
var tokens = OAuthCache.GetAndRemoveTokens(tokenKey!);
|
|
if (tokens is not null)
|
|
{
|
|
await CredStore.SetAsync(tokens);
|
|
await SessionManager.ClearAllAsync();
|
|
}
|
|
|
|
// Strip token_key from URL bar
|
|
Nav.NavigateTo(uri.GetLeftPart(UriPartial.Path), replace: true);
|
|
}
|
|
|
|
private async Task RefreshCredentialState()
|
|
{
|
|
var tokens = await CredStore.GetAsync();
|
|
_hasCredentials = tokens is not null && !string.IsNullOrEmpty(tokens.RefreshToken);
|
|
_credUsername = tokens?.UserPrincipalName ?? string.Empty;
|
|
await InvokeAsync(StateHasChanged);
|
|
}
|
|
|
|
private async Task ReconnectAsync()
|
|
{
|
|
await CredStore.ClearAsync();
|
|
await SessionManager.ClearAllAsync();
|
|
_hasCredentials = false;
|
|
_credUsername = string.Empty;
|
|
if (Session.HasProfile && _credModal is not null)
|
|
await _credModal.ShowAsync();
|
|
}
|
|
|
|
private async Task OnCredentialsConnected()
|
|
{
|
|
await RefreshCredentialState();
|
|
}
|
|
|
|
private void OnProfileChanged()
|
|
{
|
|
InvokeAsync(async () =>
|
|
{
|
|
StateHasChanged();
|
|
// New profile selected → prompt for credentials if none
|
|
if (Session.HasProfile && !_hasCredentials && _credModal is not null)
|
|
await _credModal.ShowAsync();
|
|
});
|
|
}
|
|
|
|
private void ToggleSidebar() => _sidebarCollapsed = !_sidebarCollapsed;
|
|
|
|
private static string RoleChipClass(UserRole role) => role switch
|
|
{
|
|
UserRole.Admin => "chip-red",
|
|
UserRole.TechN1 => "chip-green",
|
|
_ => "chip-blue"
|
|
};
|
|
|
|
public void Dispose()
|
|
{
|
|
Session.ProfileChanged -= OnProfileChanged;
|
|
UserContext.Initialized -= OnUserContextInitialized;
|
|
}
|
|
}
|