@bind:after did not persist reliably. Move back to an explicit @onchange
handler and surface every outcome in the page alert, including the role
re-read from the store after the write. This makes a failed save visible
(unrecognized value, exception, or saved != selected) instead of silent,
so we can pinpoint where the role update breaks.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The role <select> used a manual value=/@onchange pattern that parsed
e.Value and returned silently when the parse failed, so changing a role
did nothing and showed no message. Switch to @bind + @bind:after so the
framework handles the enum conversion, and log/verify the persisted role
in UpdateRoleAsync (now returns the previous role) for diagnosis.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Feature work:
- Certificate (app-only) auth per profile: cert store, context/Graph client
factories, automated app-registration provisioning (delegated + application
permissions, admin consent), and a SessionManager seam that resolves the auth
model per profile.
- Scheduled reports: repositories, hosted service/runner/coordinator, report
pages, and email delivery (app-only Mail.Send).
- Tenant-wide user-access audit when no site is selected.
Audit fixes:
- Site enumeration: app-only discovery used Graph getAllSites (needs Graph
Sites.Read.All the cert app lacks) and silently returned empty. Switched to
the admin-host CSOM TenantSiteEnumerator, matching the scheduler; both auth
models now share one enumeration path.
- Group expansion: the scan records a SharePoint group as a single principal, so
user-centric audits found nothing for group-granted access. Resolve group
membership (shared by audit + scheduler) and attribute it to the target user.
- M365 group claims: the resolver only recognized AAD security groups
(c:0t.c|). Group-connected/Teams sites grant via the M365 group claim
(c:0o.c|…|<guid>[_o]); now expanded too, resolving owners for the "_o" claim.
- Provision Directory.Read.All as an application permission so M365/AAD group
expansion works under the cert identity.
Also: ignore data/appcerts/ (encrypted certificate key material).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Report branding (top-left MSP logo, top-right client logo):
- Add MspLogo to AppSettings; client logo already on TenantProfile
- IUserSessionService.CurrentBranding composes MSP + active profile logo
- New reusable LogoUpload component (InputFile -> base64 LogoData, 512KB cap)
- MSP logo upload in Settings; optional client logo in profile create/edit
- Wire ReportBranding into all 6 HTML export pages
- Fix EditProfile dropping ClientLogo on edit
Storage metrics: expose folder scan depth (0-20) in scan options UI,
passed to existing StorageScanOptions.FolderDepth recursion.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "Auto-elevate ownership when permission scan is denied" setting was
dead code: the toggle was persisted but never read, the audit flow never
passed its onAccessDenied callback, and EnrichException wrapped every CSOM
error (including ServerUnauthorizedAccessException) into a generic
InvalidOperationException so the access-denied catch could never match.
Centralize elevation instead of per-call-site callbacks:
- Throw typed SharePointAccessDeniedException from EnrichException on
access-denied, preserving the failing site URL and enriched diagnostic.
- Add scoped IElevationCoordinator that catches it, and when AutoTakeOwnership
is enabled takes site-collection admin via the tenant admin endpoint and
retries the operation once. Per-site dedupe prevents loops; admin-host
denials are not treated as ownership issues. Retry is safe because each
wrapped operation closure re-issues its own CSOM loads.
- Wrap all site-scoped operations (Storage, Permissions, Duplicates, Search,
VersionCleanup, FolderStructure, BulkMembers, FileTransfer, Templates) and
the UserAccessAudit per-site scan in the coordinator.
- Drop the unused onAccessDenied parameter from IUserAccessAuditService.
Elevation still requires SharePoint tenant admin rights on the signed-in
account; the coordinator surfaces a clear message when that is missing.
Also keeps the prior StorageService change that avoids admin-gated
folder.StorageMetrics (403 for delegated non-admin tokens).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
AADSTS700016 came from the register flow sending the configured
Oidc:ClientId (still a placeholder) as the auth client. The desktop
reference app never needs config: it bootstraps with the first-party
"Microsoft Graph Command Line Tools" public client (14d82eec-...) via
MSAL interactive, which exists in every tenant.
Replicate that for the web app. A server can't do MSAL loopback and the
bootstrap client's redirect URIs don't include /connect/callback, so use
the OAuth 2.0 device authorization grant instead — the web-equivalent of
the desktop interactive flow:
- Add EntraDeviceCodeFlow: POST /devicecode then poll /token with the
bootstrap client. No backing app, no client id/secret, no redirect URI.
- Profiles "Register in Entra" now shows the verification URL + user code
and polls until the admin signs in, then calls AppRegistrationService
to create the per-client app and adopts its appId.
- Remove the dead /connect/register-initiate endpoint and the
IsRegistration branch from the callback (connect flow only now).
The client-tenant register/connect flows are now fully secretless. The
Oidc:* config is used only by the toolbox's own sign-in (unchanged).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add missing modal CSS (.modal-overlay/.modal-dialog/.modal-header):
the "Connect to Microsoft" auth modal was rendering unstyled inline
at the bottom of the page. Now a centered dialog with backdrop.
- Surface OAuth connect errors in the modal instead of silently
reopening it with no explanation.
- MainLayout: implement IDisposable so event handlers are actually
unsubscribed (Dispose existed but was never invoked).
- Wire up the Settings theme selector (was a dead control): drop the
unsupported Dark option, call sptb.setTheme on save and on load,
resolve System via prefers-color-scheme.
- Add branded 404 page via UseStatusCodePagesWithReExecute + Routes
<NotFound> (blank white page before).
- Add .progress-fill.indeterminate animation and .progress-panel.
- Home: replace inline JS hover handlers with a .feature-card CSS class.
- Define missing --surface-2 variable referenced by MainLayout.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>