@page "/profiles" @attribute [Microsoft.AspNetCore.Authorization.Authorize] @inject IUserSessionService Session @inject IUserContextAccessor UserContext @inject SharepointToolbox.Web.Infrastructure.Persistence.ProfileRepository ProfileRepo @inject SharepointToolbox.Web.Infrastructure.Auth.IAppOnlyCertStore CertStore @inject SharepointToolbox.Web.Infrastructure.Auth.IAppOnlyContextFactory AppOnlyFactory @inject ISessionCredentialStore CredStore @inject NavigationManager Nav @inject SharepointToolbox.Web.Services.OAuth.IEntraDeviceCodeFlow DeviceFlow @inject SharepointToolbox.Web.Services.Auth.IAppRegistrationService AppRegService @inject SharepointToolbox.Web.Services.Auth.ICertProvisioningService CertProvisioning @inject Microsoft.Extensions.Options.IOptions ConnectOpts @inject TranslationSource T @rendermode InteractiveServer @using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.WebUtilities @using SharepointToolbox.Web.Core.Models @using SharepointToolbox.Web.Services.Session

@T["profiles.title"]

@T["profiles.subtitle"]

@if (UserContext.Role != UserRole.Admin) { @* Non-admins can only select a profile, not create/edit/delete *@
@T["profiles.restricted"]
@foreach (var p in _profiles) {
@p.Name
@p.TenantUrl
@if (Session.CurrentProfile?.Id == p.Id) { @T["profiles.active"] }
} return; } @* Admin view — full CRUD *@ @if (!string.IsNullOrEmpty(_pageError)) {
@_pageError
}
@if (_profiles.Count == 0 && !_showForm) {
@T["profiles.empty"]
} @foreach (var p in _profiles) {
@p.Name
@p.TenantUrl
@T["profiles.tenantid.label"] @p.TenantId
@T["profiles.clientid.label"] @p.ClientId
@if (Session.CurrentProfile?.Id == p.Id) { @T["profiles.active"] }
} @if (_showForm) {
@(_editing?.Id == null ? T["profiles.form.new"] : T["profiles.form.edit"])
@if (!string.IsNullOrEmpty(_formError)) {
@_formError
}
@* App registration section *@
@T["profiles.register.hint"] @if (_deviceCode is not null) {
@T["profiles.devicecode.intro.pre"] @T["profiles.devicecode.intro.tenant"] @T["profiles.devicecode.intro.post"]
  1. @T["profiles.devicecode.step.open"] @_deviceCode.VerificationUri
  2. @T["profiles.devicecode.step.code"] @_deviceCode.UserCode
  3. @T["profiles.devicecode.step.approve"]
@_regStatus
} else if (!string.IsNullOrEmpty(_regStatus)) {
@_regStatus
}
@T["profiles.form.logo.hint"]
@* ── App-only credentials for scheduled (unattended) reports ── *@
@if (_editing is null) {
Save this client first, then re-open it to configure certificate credentials.
} else { When enabled, this client uses a certificate-based app registration with application permissions (Sites.FullControl.All, admin-consented) for both interactive work and scheduled reports. Technicians never sign in to SharePoint per profile. The Register app button provisions the certificate and consent automatically; the fields below are for manual setup. @if (_certPresent) {
Certificate stored @if (!string.IsNullOrEmpty(_form.AppOnlyCertThumbprint)) { @_form.AppOnlyCertThumbprint }
} else {
}
@if (!string.IsNullOrEmpty(_appOnlyStatus)) { @_appOnlyStatus }
}
} @code { private List _profiles = new(); private bool _showForm; private bool _registering; private TenantProfile? _editing; private TenantProfile _form = new(); private string _formError = string.Empty; private string _pageError = string.Empty; private SharepointToolbox.Web.Services.OAuth.DeviceCodeStart? _deviceCode; private string _regStatus = string.Empty; private CancellationTokenSource? _regCts; // Graph delegated scopes the admin must consent to so we can create the app registration, // attach the certificate, and grant application-permission (app-role) admin consent. private const string RegistrationScope = "https://graph.microsoft.com/Application.ReadWrite.All " + "https://graph.microsoft.com/DelegatedPermissionGrant.ReadWrite.All " + "https://graph.microsoft.com/AppRoleAssignment.ReadWrite.All " + "https://graph.microsoft.com/Directory.Read.All " + "openid offline_access"; private bool CanRegister => !string.IsNullOrWhiteSpace(_form.Name) && !string.IsNullOrWhiteSpace(_form.TenantUrl) && !string.IsNullOrWhiteSpace(_form.TenantId); protected override async Task OnInitializedAsync() { _profiles = (await ProfileRepo.LoadAsync()).ToList(); } protected override async Task OnAfterRenderAsync(bool firstRender) { if (!firstRender) return; await HandleConnectErrorAsync(); } private async Task HandleConnectErrorAsync() { var uri = new Uri(Nav.Uri); var query = QueryHelpers.ParseQuery(uri.Query); if (!query.TryGetValue("connect_error", out var err) || string.IsNullOrEmpty(err)) return; _pageError = err!; await InvokeAsync(StateHasChanged); Nav.NavigateTo(uri.GetLeftPart(UriPartial.Path), replace: true); } private void AddNew() { _editing = null; _form = new TenantProfile(); _showForm = true; _formError = string.Empty; _pageError = string.Empty; } private void EditProfile(TenantProfile p) { _editing = p; _form = new TenantProfile { Id = p.Id, Name = p.Name, TenantUrl = p.TenantUrl, TenantId = p.TenantId, ClientId = p.ClientId, ClientLogo = p.ClientLogo, AppOnlyEnabled = p.AppOnlyEnabled, AppOnlyClientId = p.AppOnlyClientId, AppOnlyCertThumbprint = p.AppOnlyCertThumbprint }; _showForm = true; _formError = _pageError = string.Empty; _certPresent = CertStore.Exists(p.Id); _pfxBytes = null; _certPassword = string.Empty; _appOnlyStatus = string.Empty; } private void CancelForm() { _showForm = false; _editing = null; } private void SelectProfile(TenantProfile p) { Session.SetProfile(p); StateHasChanged(); } private async Task RegisterAppAsync() { if (!CanRegister || _registering) return; _registering = true; _formError = string.Empty; _regStatus = T["profiles.reg.requesting"]; _deviceCode = null; _regCts = new CancellationTokenSource(TimeSpan.FromMinutes(15)); StateHasChanged(); try { // Secretless bootstrap: device code flow against the client tenant. _deviceCode = await DeviceFlow.BeginAsync(_form.TenantId.Trim(), RegistrationScope, _regCts.Token); _regStatus = T["profiles.reg.waitingsignin"]; StateHasChanged(); var adminToken = await DeviceFlow.PollForAccessTokenAsync(_form.TenantId.Trim(), _deviceCode, _regCts.Token); _deviceCode = null; _regStatus = T["profiles.reg.creating"]; StateHasChanged(); // Generate + store the app-only certificate before creating the registration so its // public key can be attached as a sign-in credential. Technicians then operate under // the app identity and never sign in to SharePoint per profile. var cert = await CertProvisioning.GenerateAndStoreAsync(_form.Id, $"SP Toolbox — {_form.Name}", _regCts.Token); var clientId = await AppRegService.CreateAsync( adminAccessToken: adminToken, tenantName: _form.Name, redirectUri: ConnectOpts.Value.RedirectUri, appOnlyCert: cert, ct: _regCts.Token); _form.ClientId = clientId; _form.AppOnlyClientId = clientId; _form.AppOnlyEnabled = true; _form.AppOnlyCertThumbprint = cert.Thumbprint; _certPresent = true; // Cert key credential + app-role consent take time to propagate through Entra; // wait it out so the profile is usable immediately instead of 401ing on first use. _regStatus = T["profiles.reg.propagating"]; StateHasChanged(); var notReady = await AppOnlyFactory.WaitUntilReadyAsync(_form, TimeSpan.FromSeconds(90), _regCts.Token); _regStatus = notReady is null ? T["profiles.reg.registered"] : string.Format(T["profiles.reg.notready"], notReady); } catch (OperationCanceledException) { _regStatus = T["profiles.reg.cancelled"]; } catch (Exception ex) { _formError = string.Format(T["profiles.reg.failed"], ex.Message); _regStatus = string.Empty; } finally { _deviceCode = null; _registering = false; _regCts?.Dispose(); _regCts = null; StateHasChanged(); } } private void CancelRegistration() { _regCts?.Cancel(); _deviceCode = null; _regStatus = T["profiles.reg.cancelled"]; } private async Task SaveProfile() { _formError = string.Empty; if (string.IsNullOrWhiteSpace(_form.Name)) { _formError = T["profiles.err.name_required"]; return; } if (string.IsNullOrWhiteSpace(_form.TenantUrl)) { _formError = T["profiles.err.url_required"]; return; } if (string.IsNullOrWhiteSpace(_form.ClientId)) { _formError = T["profiles.err.clientid_required"]; return; } if (string.IsNullOrWhiteSpace(_form.TenantId)) { _formError = T["profiles.err.tenantid_required"]; return; } if (_editing == null) { _form.Id = Guid.NewGuid().ToString(); _profiles.Add(_form); } else { var idx = _profiles.FindIndex(p => p.Id == _editing.Id); if (idx >= 0) _profiles[idx] = _form; } await ProfileRepo.SaveAsync(_profiles); _showForm = false; _editing = null; } private async Task DeleteProfile(TenantProfile p) { _profiles.RemoveAll(x => x.Id == p.Id); await ProfileRepo.SaveAsync(_profiles); CertStore.Delete(p.Id); if (Session.CurrentProfile?.Id == p.Id) await Session.ClearSessionAsync(); } // ── App-only credential handlers ─────────────────────────────────────────── private const long MaxCertBytes = 256 * 1024; private byte[]? _pfxBytes; private string _certPassword = string.Empty; private bool _certPresent; private bool _appOnlyTesting; private string _appOnlyStatus = string.Empty; private async Task OnCertSelected(InputFileChangeEventArgs e) { _appOnlyStatus = string.Empty; var file = e.File; if (file is null) return; if (file.Size > MaxCertBytes) { _appOnlyStatus = $"Certificate too large (max {MaxCertBytes / 1024} KB)."; _pfxBytes = null; return; } try { using var ms = new MemoryStream(); await file.OpenReadStream(MaxCertBytes).CopyToAsync(ms); _pfxBytes = ms.ToArray(); } catch (Exception ex) { _appOnlyStatus = $"Failed to read certificate: {ex.Message}"; _pfxBytes = null; } } private async Task UploadCertAsync() { if (_pfxBytes is null || _editing is null) return; try { var thumbprint = await CertStore.SaveAsync(_form.Id, _pfxBytes, string.IsNullOrEmpty(_certPassword) ? null : _certPassword); _form.AppOnlyCertThumbprint = thumbprint; _certPresent = true; _pfxBytes = null; _certPassword = string.Empty; await PersistFormAsync(); _appOnlyStatus = "Certificate stored."; } catch (Exception ex) { _appOnlyStatus = $"Certificate rejected: {ex.Message}"; } } private async Task RemoveCertAsync() { if (_editing is null) return; CertStore.Delete(_form.Id); _certPresent = false; _form.AppOnlyCertThumbprint = string.Empty; await PersistFormAsync(); _appOnlyStatus = "Certificate removed."; } private async Task TestAppOnlyAsync() { if (_editing is null) return; _appOnlyTesting = true; _appOnlyStatus = string.Empty; try { // Persist current field edits first so the test uses what the admin sees. await PersistFormAsync(); var probe = new TenantProfile { Id = _form.Id, Name = _form.Name, TenantUrl = _form.TenantUrl, TenantId = _form.TenantId, AppOnlyEnabled = true, AppOnlyClientId = _form.AppOnlyClientId }; var error = await AppOnlyFactory.TestConnectionAsync(probe); _appOnlyStatus = error is null ? "✓ Connected successfully." : $"✗ {error}"; } catch (Exception ex) { _appOnlyStatus = $"✗ {ex.Message}"; } finally { _appOnlyTesting = false; } } // Upserts the in-progress form into the profile list and saves, without closing the form. private async Task PersistFormAsync() { var idx = _profiles.FindIndex(p => p.Id == _form.Id); if (idx >= 0) _profiles[idx] = _form; else _profiles.Add(_form); await ProfileRepo.SaveAsync(_profiles); } }