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

Client Profiles

Manage SharePoint tenant connections. Credentials are entered per session — no secrets stored on disk.

@if (UserContext.Role != UserRole.Admin) { @* Non-admins can only select a profile, not create/edit/delete *@
Profile management is restricted to Admins. Select a profile below to work on a client.
@foreach (var p in _profiles) {
@p.Name
@p.TenantUrl
@if (Session.CurrentProfile?.Id == p.Id) { Active }
} return; } @* Admin view — full CRUD *@ @if (!string.IsNullOrEmpty(_pageError)) {
@_pageError
}
@if (_profiles.Count == 0 && !_showForm) {
No profiles configured. Create one to get started.
} @foreach (var p in _profiles) {
@p.Name
@p.TenantUrl
Tenant ID: @p.TenantId
Client ID: @p.ClientId
@if (Session.CurrentProfile?.Id == p.Id) { Active }
} @if (_showForm) {
@(_editing?.Id == null ? "New Profile" : "Edit Profile")
@if (!string.IsNullOrEmpty(_formError)) {
@_formError
}
@* App registration section *@
Click "Register in Entra" to auto-create the app registration in the client tenant. You'll sign in with a client admin account — no secrets, no pre-existing app needed. Or enter an existing public client App Registration ID manually. @if (_deviceCode is not null) {
Sign in to the client tenant to authorize app creation:
  1. Open @_deviceCode.VerificationUri
  2. Enter code: @_deviceCode.UserCode
  3. Approve the requested permissions with an admin account.
@_regStatus
} else if (!string.IsNullOrEmpty(_regStatus)) {
@_regStatus
}
} @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. private const string RegistrationScope = "https://graph.microsoft.com/Application.ReadWrite.All " + "https://graph.microsoft.com/DelegatedPermissionGrant.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 }; _showForm = true; _formError = _pageError = 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 = "Requesting a sign-in code…"; _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 = "Waiting for sign-in to complete…"; StateHasChanged(); var adminToken = await DeviceFlow.PollForAccessTokenAsync(_form.TenantId.Trim(), _deviceCode, _regCts.Token); _deviceCode = null; _regStatus = "Creating the app registration…"; StateHasChanged(); var clientId = await AppRegService.CreateAsync( adminAccessToken: adminToken, tenantName: _form.Name, redirectUri: ConnectOpts.Value.RedirectUri, ct: _regCts.Token); _form.ClientId = clientId; _regStatus = "App registered. Review and Save the profile."; } catch (OperationCanceledException) { _regStatus = "Registration cancelled."; } catch (Exception ex) { _formError = $"Registration failed: {ex.Message}"; _regStatus = string.Empty; } finally { _deviceCode = null; _registering = false; _regCts?.Dispose(); _regCts = null; StateHasChanged(); } } private void CancelRegistration() { _regCts?.Cancel(); _deviceCode = null; _regStatus = "Registration cancelled."; } private async Task SaveProfile() { _formError = string.Empty; if (string.IsNullOrWhiteSpace(_form.Name)) { _formError = "Name is required."; return; } if (string.IsNullOrWhiteSpace(_form.TenantUrl)) { _formError = "Tenant URL is required."; return; } if (string.IsNullOrWhiteSpace(_form.ClientId)) { _formError = "Client ID is required."; return; } if (string.IsNullOrWhiteSpace(_form.TenantId)) { _formError = "Tenant ID is 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); if (Session.CurrentProfile?.Id == p.Id) await Session.ClearSessionAsync(); } }