@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 @inject TranslationSource T @rendermode InteractiveServer @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"]
} @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, ClientLogo = p.ClientLogo }; _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 = 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(); var clientId = await AppRegService.CreateAsync( adminAccessToken: adminToken, tenantName: _form.Name, redirectUri: ConnectOpts.Value.RedirectUri, ct: _regCts.Token); _form.ClientId = clientId; _regStatus = T["profiles.reg.registered"]; } 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); if (Session.CurrentProfile?.Id == p.Id) await Session.ClearSessionAsync(); } }