startup flow optimized (performance)

This commit is contained in:
Oliver Kieselbach
2026-04-04 14:19:53 +02:00
parent 9fdab5fb97
commit b6d641b06d
8 changed files with 560 additions and 88 deletions
@@ -1 +1 @@
830
831
@@ -0,0 +1,346 @@
using System.Net;
using AutopilotMonitor.Functions.Functions.Infrastructure;
using AutopilotMonitor.Functions.Services;
using AutopilotMonitor.Shared;
using AutopilotMonitor.Shared.Models;
namespace AutopilotMonitor.Functions.Tests;
/// <summary>
/// Tests for AuthFunction.BuildAuthResult — the pure decision logic behind GET /api/auth/me.
/// Covers all gate checks and edge cases to guard against regressions when parallelizing queries.
/// </summary>
public class AuthFunctionTests
{
private const string TenantId = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
private const string Upn = "user@contoso.com";
private const string DisplayName = "Test User";
private const string ObjectId = "oid-12345";
private static TenantConfiguration DefaultConfig() => TenantConfiguration.CreateDefault(TenantId);
private static McpAccessCheckResult McpAllowed() => McpAccessCheckResult.Allowed(Upn, "test-grant");
private static McpAccessCheckResult McpDenied() => McpAccessCheckResult.Denied("not allowed");
private static MemberRoleInfo AdminRole() => new() { Role = Constants.TenantRoles.Admin, CanManageBootstrapTokens = true };
private static MemberRoleInfo OperatorRole() => new() { Role = Constants.TenantRoles.Operator, CanManageBootstrapTokens = false };
private static MemberRoleInfo ViewerRole() => new() { Role = Constants.TenantRoles.Viewer, CanManageBootstrapTokens = false };
// -------------------------------------------------------------------------
// Happy Path
// -------------------------------------------------------------------------
[Fact]
public void HappyPath_AdminUser_Returns200WithCorrectFields()
{
var result = AuthFunction.BuildAuthResult(
DefaultConfig(), isGlobalAdmin: false, isPreviewApproved: true,
memberRole: AdminRole(), mcpCheck: McpAllowed(),
hasTenantAdmins: true,
TenantId, Upn, DisplayName, ObjectId);
Assert.True(result.IsSuccess);
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
Assert.False(result.NeedsAutoAdmin);
var body = ToDynamic(result.Body);
Assert.Equal(TenantId, (string)body.tenantId);
Assert.Equal(Upn, (string)body.upn);
Assert.Equal(DisplayName, (string)body.displayName);
Assert.Equal(ObjectId, (string)body.objectId);
Assert.False((bool)body.isGlobalAdmin);
Assert.True((bool)body.isTenantAdmin);
Assert.Equal(Constants.TenantRoles.Admin, (string)body.role);
Assert.True((bool)body.canManageBootstrapTokens);
Assert.True((bool)body.hasMcpAccess);
}
[Fact]
public void HappyPath_OperatorUser_ReturnsCorrectRole()
{
var result = AuthFunction.BuildAuthResult(
DefaultConfig(), isGlobalAdmin: false, isPreviewApproved: true,
memberRole: OperatorRole(), mcpCheck: McpDenied(),
hasTenantAdmins: true,
TenantId, Upn, DisplayName, ObjectId);
Assert.True(result.IsSuccess);
var body = ToDynamic(result.Body);
Assert.Equal(Constants.TenantRoles.Operator, (string)body.role);
Assert.False((bool)body.isTenantAdmin);
Assert.False((bool)body.canManageBootstrapTokens);
Assert.False((bool)body.hasMcpAccess);
}
[Fact]
public void HappyPath_ViewerUser_ReturnsCorrectRole()
{
var result = AuthFunction.BuildAuthResult(
DefaultConfig(), isGlobalAdmin: false, isPreviewApproved: true,
memberRole: ViewerRole(), mcpCheck: McpDenied(),
hasTenantAdmins: true,
TenantId, Upn, DisplayName, ObjectId);
Assert.True(result.IsSuccess);
var body = ToDynamic(result.Body);
Assert.Equal(Constants.TenantRoles.Viewer, (string)body.role);
Assert.False((bool)body.isTenantAdmin);
}
[Fact]
public void HappyPath_GlobalAdmin_ReturnsIsGlobalAdminTrue()
{
var result = AuthFunction.BuildAuthResult(
DefaultConfig(), isGlobalAdmin: true, isPreviewApproved: true,
memberRole: AdminRole(), mcpCheck: McpAllowed(),
hasTenantAdmins: true,
TenantId, Upn, DisplayName, ObjectId);
Assert.True(result.IsSuccess);
var body = ToDynamic(result.Body);
Assert.True((bool)body.isGlobalAdmin);
}
// -------------------------------------------------------------------------
// Gate 1: Suspended Tenant
// -------------------------------------------------------------------------
[Fact]
public void SuspendedTenant_Returns403WithTenantSuspendedError()
{
var config = DefaultConfig();
config.Disabled = true;
var result = AuthFunction.BuildAuthResult(
config, isGlobalAdmin: false, isPreviewApproved: true,
memberRole: AdminRole(), mcpCheck: McpAllowed(),
hasTenantAdmins: true,
TenantId, Upn, DisplayName, ObjectId);
Assert.False(result.IsSuccess);
Assert.Equal(HttpStatusCode.Forbidden, result.StatusCode);
var body = ToDynamic(result.Body);
Assert.Equal("TenantSuspended", (string)body.error);
}
[Fact]
public void SuspendedTenant_WithCustomReason_ReturnsCustomMessage()
{
var config = DefaultConfig();
config.Disabled = true;
config.DisabledReason = "Terms of service violation";
var result = AuthFunction.BuildAuthResult(
config, isGlobalAdmin: false, isPreviewApproved: true,
memberRole: AdminRole(), mcpCheck: McpAllowed(),
hasTenantAdmins: true,
TenantId, Upn, DisplayName, ObjectId);
Assert.False(result.IsSuccess);
var body = ToDynamic(result.Body);
Assert.Equal("Terms of service violation", (string)body.message);
}
[Fact]
public void SuspendedTenant_BlocksEvenGlobalAdmin()
{
var config = DefaultConfig();
config.Disabled = true;
var result = AuthFunction.BuildAuthResult(
config, isGlobalAdmin: true, isPreviewApproved: true,
memberRole: AdminRole(), mcpCheck: McpAllowed(),
hasTenantAdmins: true,
TenantId, Upn, DisplayName, ObjectId);
Assert.False(result.IsSuccess);
Assert.Equal(HttpStatusCode.Forbidden, result.StatusCode);
var body = ToDynamic(result.Body);
Assert.Equal("TenantSuspended", (string)body.error);
}
[Fact]
public void SuspendedTenant_WithExpiredDisabledUntil_IsNotBlocked()
{
var config = DefaultConfig();
config.Disabled = true;
config.DisabledUntil = DateTime.UtcNow.AddHours(-1); // expired
var result = AuthFunction.BuildAuthResult(
config, isGlobalAdmin: false, isPreviewApproved: true,
memberRole: AdminRole(), mcpCheck: McpAllowed(),
hasTenantAdmins: true,
TenantId, Upn, DisplayName, ObjectId);
Assert.True(result.IsSuccess);
}
// -------------------------------------------------------------------------
// Gate 2: Preview Gate
// -------------------------------------------------------------------------
[Fact]
public void PreviewGate_NotApproved_Returns403PrivatePreview()
{
var result = AuthFunction.BuildAuthResult(
DefaultConfig(), isGlobalAdmin: false, isPreviewApproved: false,
memberRole: AdminRole(), mcpCheck: McpAllowed(),
hasTenantAdmins: true,
TenantId, Upn, DisplayName, ObjectId);
Assert.False(result.IsSuccess);
Assert.Equal(HttpStatusCode.Forbidden, result.StatusCode);
var body = ToDynamic(result.Body);
Assert.Equal("PrivatePreview", (string)body.error);
}
[Fact]
public void PreviewGate_GlobalAdminBypassesPreviewGate()
{
var result = AuthFunction.BuildAuthResult(
DefaultConfig(), isGlobalAdmin: true, isPreviewApproved: false,
memberRole: AdminRole(), mcpCheck: McpAllowed(),
hasTenantAdmins: true,
TenantId, Upn, DisplayName, ObjectId);
Assert.True(result.IsSuccess);
}
// -------------------------------------------------------------------------
// Auto-Admin Logic
// -------------------------------------------------------------------------
[Fact]
public void AutoAdmin_FirstUser_NoExistingAdmins_BecomesAdmin()
{
var result = AuthFunction.BuildAuthResult(
DefaultConfig(), isGlobalAdmin: false, isPreviewApproved: true,
memberRole: null, // no role yet
mcpCheck: McpDenied(),
hasTenantAdmins: false, // no existing admins
TenantId, Upn, DisplayName, ObjectId);
Assert.True(result.IsSuccess);
Assert.True(result.NeedsAutoAdmin);
var body = ToDynamic(result.Body);
Assert.True((bool)body.isTenantAdmin);
Assert.Equal(Constants.TenantRoles.Admin, (string)body.role);
Assert.True((bool)body.canManageBootstrapTokens);
}
[Fact]
public void AutoAdmin_NotTriggered_WhenAdminsExist()
{
var result = AuthFunction.BuildAuthResult(
DefaultConfig(), isGlobalAdmin: false, isPreviewApproved: true,
memberRole: null, // no role — new user but admins exist
mcpCheck: McpDenied(),
hasTenantAdmins: true, // admins already exist
TenantId, Upn, DisplayName, ObjectId);
Assert.True(result.IsSuccess);
Assert.False(result.NeedsAutoAdmin);
var body = ToDynamic(result.Body);
Assert.False((bool)body.isTenantAdmin);
Assert.Null((string?)body.role);
}
[Fact]
public void AutoAdmin_NotTriggered_WhenUserAlreadyAdmin()
{
var result = AuthFunction.BuildAuthResult(
DefaultConfig(), isGlobalAdmin: false, isPreviewApproved: true,
memberRole: AdminRole(),
mcpCheck: McpDenied(),
hasTenantAdmins: true,
TenantId, Upn, DisplayName, ObjectId);
Assert.True(result.IsSuccess);
Assert.False(result.NeedsAutoAdmin);
var body = ToDynamic(result.Body);
Assert.True((bool)body.isTenantAdmin);
}
// -------------------------------------------------------------------------
// MCP Access
// -------------------------------------------------------------------------
[Fact]
public void McpAccess_Allowed_ReturnsTrue()
{
var result = AuthFunction.BuildAuthResult(
DefaultConfig(), isGlobalAdmin: false, isPreviewApproved: true,
memberRole: AdminRole(), mcpCheck: McpAllowed(),
hasTenantAdmins: true,
TenantId, Upn, DisplayName, ObjectId);
var body = ToDynamic(result.Body);
Assert.True((bool)body.hasMcpAccess);
}
[Fact]
public void McpAccess_Denied_ReturnsFalse()
{
var result = AuthFunction.BuildAuthResult(
DefaultConfig(), isGlobalAdmin: false, isPreviewApproved: true,
memberRole: AdminRole(), mcpCheck: McpDenied(),
hasTenantAdmins: true,
TenantId, Upn, DisplayName, ObjectId);
var body = ToDynamic(result.Body);
Assert.False((bool)body.hasMcpAccess);
}
// -------------------------------------------------------------------------
// Gate Priority: Suspended takes precedence over Preview
// -------------------------------------------------------------------------
[Fact]
public void GatePriority_SuspendedTakesPrecedenceOverPreviewGate()
{
var config = DefaultConfig();
config.Disabled = true;
var result = AuthFunction.BuildAuthResult(
config, isGlobalAdmin: false, isPreviewApproved: false,
memberRole: null, mcpCheck: McpDenied(),
hasTenantAdmins: false,
TenantId, Upn, DisplayName, ObjectId);
Assert.False(result.IsSuccess);
var body = ToDynamic(result.Body);
Assert.Equal("TenantSuspended", (string)body.error);
}
// -------------------------------------------------------------------------
// ExtractDomainFromUpn
// -------------------------------------------------------------------------
[Theory]
[InlineData("user@contoso.com", "contoso.com")]
[InlineData("admin@sub.domain.org", "sub.domain.org")]
[InlineData("", "")]
[InlineData("noemail", "")]
public void ExtractDomainFromUpn_ReturnsExpected(string upn, string expected)
{
Assert.Equal(expected, AuthFunction.ExtractDomainFromUpn(upn));
}
// -------------------------------------------------------------------------
// Helper
// -------------------------------------------------------------------------
/// <summary>
/// Converts anonymous object to dynamic for property access in tests.
/// </summary>
private static dynamic ToDynamic(object obj)
{
var type = obj.GetType();
var dict = new System.Dynamic.ExpandoObject() as IDictionary<string, object?>;
foreach (var prop in type.GetProperties())
{
dict[prop.Name] = prop.GetValue(obj);
}
return dict;
}
}
@@ -9,6 +9,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@@ -1,7 +1,9 @@
using System.Net;
using AutopilotMonitor.Functions.Extensions;
using AutopilotMonitor.Functions.Services;
using AutopilotMonitor.Shared;
using AutopilotMonitor.Shared.DataAccess;
using AutopilotMonitor.Shared.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Functions.Worker;
@@ -79,8 +81,25 @@ public class AuthFunction
return badRequestResponse;
}
// Load tenant configuration
var tenantConfig = await _tenantConfigService.GetConfigurationAsync(tenantId);
// --- Parallel data fetch: all independent queries run concurrently ---
var tenantConfigTask = _tenantConfigService.GetConfigurationAsync(tenantId);
var isGlobalAdminTask = _globalAdminService.IsGlobalAdminAsync(upn);
var isApprovedTask = _previewWhitelistService.IsApprovedAsync(tenantId);
var memberRoleTask = _tenantAdminsService.GetMemberRoleAsync(tenantId, upn);
var mcpCheckTask = _mcpUserService.IsAllowedAsync(upn);
var existingAdminsTask = _tenantAdminsService.GetTenantAdminsAsync(tenantId);
await Task.WhenAll(tenantConfigTask, isGlobalAdminTask, isApprovedTask,
memberRoleTask, mcpCheckTask, existingAdminsTask);
var tenantConfig = tenantConfigTask.Result;
var isGlobalAdmin = isGlobalAdminTask.Result;
var isApproved = isApprovedTask.Result;
var memberRole = memberRoleTask.Result;
var mcpCheck = mcpCheckTask.Result;
var existingAdmins = existingAdminsTask.Result;
// --- Side-effects that don't affect the auth decision ---
// Extract and save domain name from first user if not set
if (string.IsNullOrEmpty(tenantConfig.DomainName) && !string.IsNullOrEmpty(upn))
@@ -88,7 +107,7 @@ public class AuthFunction
var domain = ExtractDomainFromUpn(upn);
if (!string.IsNullOrEmpty(domain))
{
_logger.LogInformation($"Setting domain name for tenant {tenantId}: {domain}");
_logger.LogInformation("Setting domain name for tenant {TenantId}: {Domain}", tenantId, domain);
tenantConfig.DomainName = domain;
tenantConfig.UpdatedBy = upn;
await _tenantConfigService.SaveConfigurationAsync(tenantConfig);
@@ -107,28 +126,11 @@ public class AuthFunction
}
}
// Check if tenant is disabled/suspended
if (tenantConfig.IsCurrentlyDisabled())
{
_logger.LogWarning($"Login attempt for suspended tenant: {tenantId} by user {upn}");
var suspendedResponse = req.CreateResponse(HttpStatusCode.Forbidden);
await suspendedResponse.WriteAsJsonAsync(new
{
error = "TenantSuspended",
message = !string.IsNullOrEmpty(tenantConfig.DisabledReason)
? tenantConfig.DisabledReason
: "Your tenant has been suspended. Please contact support for more information.",
disabledUntil = tenantConfig.DisabledUntil?.ToString("o"),
contactSupport = true
});
return suspendedResponse;
}
// Auto-re-enable: if Disabled was true but DisabledUntil has expired, persist the re-enable
if (tenantConfig.Disabled && !tenantConfig.IsCurrentlyDisabled())
{
_logger.LogInformation($"Tenant {tenantId} auto-re-enabled: DisabledUntil ({tenantConfig.DisabledUntil:o}) has expired");
_logger.LogInformation("Tenant {TenantId} auto-re-enabled: DisabledUntil ({DisabledUntil}) has expired",
tenantId, tenantConfig.DisabledUntil?.ToString("o"));
tenantConfig.Disabled = false;
tenantConfig.DisabledReason = null;
tenantConfig.DisabledUntil = null;
@@ -136,67 +138,46 @@ public class AuthFunction
await _tenantConfigService.SaveConfigurationAsync(tenantConfig);
}
// Check if user is global admin (must happen before preview gate)
var isGlobalAdmin = await _globalAdminService.IsGlobalAdminAsync(upn);
// --- Pure decision logic (tested by AuthFunctionTests) ---
var decision = BuildAuthResult(
tenantConfig, isGlobalAdmin, isApproved,
memberRole, mcpCheck, existingAdmins.Count > 0,
tenantId, upn, displayName ?? string.Empty, objectId ?? string.Empty);
// Preview gate: only approved tenants get full portal access.
// Global Admins bypass the gate (they need access to manage the whitelist).
if (!isGlobalAdmin && !await _previewWhitelistService.IsApprovedAsync(tenantId))
if (!decision.IsSuccess)
{
_logger.LogInformation("Tenant {TenantId} blocked by preview gate (user: {Upn})", tenantId, upn);
var previewResponse = req.CreateResponse(HttpStatusCode.Forbidden);
await previewResponse.WriteAsJsonAsync(new
if (decision.StatusCode == HttpStatusCode.Forbidden)
{
error = "PrivatePreview",
message = "Autopilot Monitor is currently in Private Preview. Your organization is on the waitlist \u2014 we'll notify you when access is granted."
});
return previewResponse;
}
var bodyType = decision.Body.GetType();
var errorProp = bodyType.GetProperty("error");
var errorValue = errorProp?.GetValue(decision.Body) as string;
// Check if user is tenant admin
bool isTenantAdmin = await _tenantAdminsService.IsTenantAdminAsync(tenantId, upn);
// Auto-admin logic: First user becomes admin
if (!isTenantAdmin)
{
// Check if tenant has any admins at all
var existingAdmins = await _tenantAdminsService.GetTenantAdminsAsync(tenantId);
if (existingAdmins.Count == 0)
{
_logger.LogInformation($"First user login for tenant {tenantId}: {upn} - Auto-assigning as admin");
await _tenantAdminsService.AddTenantAdminAsync(tenantId, upn, "System");
isTenantAdmin = true;
if (errorValue == "TenantSuspended")
_logger.LogWarning("Login attempt for suspended tenant: {TenantId} by user {Upn}", tenantId, upn);
else if (errorValue == "PrivatePreview")
_logger.LogInformation("Tenant {TenantId} blocked by preview gate (user: {Upn})", tenantId, upn);
}
var blockedResponse = req.CreateResponse(decision.StatusCode);
await blockedResponse.WriteAsJsonAsync(decision.Body);
return blockedResponse;
}
// Get role info for the user (Admin, Operator, Viewer, or null if not a member)
var memberRole = await _tenantAdminsService.GetMemberRoleAsync(tenantId, upn);
string? role = memberRole?.Role;
bool canManageBootstrapTokens = memberRole?.CanManageBootstrapTokens ?? false;
// --- Post-decision side-effects ---
// Auto-admin: first user becomes admin (flagged by BuildAuthResult)
if (decision.NeedsAutoAdmin)
{
_logger.LogInformation("First user login for tenant {TenantId}: {Upn} - Auto-assigning as admin", tenantId, upn);
await _tenantAdminsService.AddTenantAdminAsync(tenantId, upn, "System");
}
// Record user login activity for metrics tracking
_ = _metricsRepo.RecordUserLoginAsync(tenantId, upn, displayName, objectId)
.ContinueWith(t => _logger.LogWarning(t.Exception?.InnerException, "Fire-and-forget RecordUserLoginAsync failed"), TaskContinuationOptions.OnlyOnFaulted);
// Check MCP access (cached, lightweight)
var mcpCheck = await _mcpUserService.IsAllowedAsync(upn);
var userInfo = new
{
tenantId,
upn,
displayName,
objectId,
isGlobalAdmin,
isTenantAdmin,
role,
canManageBootstrapTokens,
hasMcpAccess = mcpCheck.IsAllowed
};
var response = req.CreateResponse(HttpStatusCode.OK);
await response.WriteAsJsonAsync(userInfo);
await response.WriteAsJsonAsync(decision.Body);
return response;
}
@@ -307,10 +288,74 @@ public class AuthFunction
return response;
}
/// <summary>
/// Pure decision logic for auth/me — no I/O, fully testable.
/// Takes all pre-fetched data and returns the auth decision.
/// </summary>
internal static AuthDecisionResult BuildAuthResult(
TenantConfiguration tenantConfig,
bool isGlobalAdmin,
bool isPreviewApproved,
MemberRoleInfo? memberRole,
McpAccessCheckResult mcpCheck,
bool hasTenantAdmins,
string tenantId, string upn, string displayName, string objectId)
{
// Gate 1: Suspended tenant
if (tenantConfig.IsCurrentlyDisabled())
{
return AuthDecisionResult.Blocked(HttpStatusCode.Forbidden, new
{
error = "TenantSuspended",
message = !string.IsNullOrEmpty(tenantConfig.DisabledReason)
? tenantConfig.DisabledReason
: "Your tenant has been suspended. Please contact support for more information.",
disabledUntil = tenantConfig.DisabledUntil?.ToString("o"),
contactSupport = true
});
}
// Gate 2: Preview gate (Global Admins bypass)
if (!isGlobalAdmin && !isPreviewApproved)
{
return AuthDecisionResult.Blocked(HttpStatusCode.Forbidden, new
{
error = "PrivatePreview",
message = "Autopilot Monitor is currently in Private Preview. Your organization is on the waitlist \u2014 we'll notify you when access is granted."
});
}
// Determine admin status: auto-admin if first user (no existing admins and no role yet)
bool isTenantAdmin = memberRole?.Role == Constants.TenantRoles.Admin;
bool needsAutoAdmin = !isTenantAdmin && !hasTenantAdmins;
if (needsAutoAdmin)
{
isTenantAdmin = true;
}
// After auto-admin, re-derive role: the auto-admin gets Admin role,
// otherwise use the fetched memberRole.
string? role = needsAutoAdmin ? Constants.TenantRoles.Admin : memberRole?.Role;
bool canManageBootstrapTokens = needsAutoAdmin || (memberRole?.CanManageBootstrapTokens ?? false);
return AuthDecisionResult.Success(new
{
tenantId,
upn,
displayName,
objectId,
isGlobalAdmin,
isTenantAdmin,
role,
canManageBootstrapTokens,
hasMcpAccess = mcpCheck.IsAllowed
}, needsAutoAdmin);
}
/// <summary>
/// Extracts domain name from UPN (e.g., user@contoso.com -> contoso.com)
/// </summary>
private static string ExtractDomainFromUpn(string upn)
internal static string ExtractDomainFromUpn(string upn)
{
if (string.IsNullOrEmpty(upn))
return string.Empty;
@@ -325,6 +370,32 @@ public class AuthFunction
}
}
/// <summary>
/// Result of the pure auth decision logic — no HTTP concerns.
/// </summary>
internal class AuthDecisionResult
{
public HttpStatusCode StatusCode { get; init; }
public object Body { get; init; } = default!;
public bool IsSuccess => StatusCode == HttpStatusCode.OK;
/// <summary>True when the user should be auto-promoted to tenant admin (first user).</summary>
public bool NeedsAutoAdmin { get; init; }
public static AuthDecisionResult Success(object body, bool needsAutoAdmin = false) => new()
{
StatusCode = HttpStatusCode.OK,
Body = body,
NeedsAutoAdmin = needsAutoAdmin
};
public static AuthDecisionResult Blocked(HttpStatusCode statusCode, object body) => new()
{
StatusCode = statusCode,
Body = body
};
}
public class AddGlobalAdminRequest
{
public string Upn { get; set; } = string.Empty;
@@ -1,3 +1,4 @@
using System.Threading;
using Azure;
using Azure.Data.Tables;
using Azure.Identity;
@@ -87,23 +88,27 @@ namespace AutopilotMonitor.Functions.Services
var successCount = 0;
var failCount = 0;
foreach (var tableName in Constants.TableNames.All)
{
try
await Parallel.ForEachAsync(
Constants.TableNames.All,
new ParallelOptions { MaxDegreeOfParallelism = 8 },
async (tableName, ct) =>
{
await _tableServiceClient.CreateTableIfNotExistsAsync(tableName);
_logger.LogDebug($"Table '{tableName}' initialized");
successCount++;
}
catch (Exception ex)
{
_logger.LogError(ex, $"Failed to initialize table '{tableName}'");
failCount++;
}
}
try
{
await _tableServiceClient.CreateTableIfNotExistsAsync(tableName, ct);
_logger.LogDebug("Table '{TableName}' initialized", tableName);
Interlocked.Increment(ref successCount);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to initialize table '{TableName}'", tableName);
Interlocked.Increment(ref failCount);
}
});
stopwatch.Stop();
_logger.LogInformation($"Table initialization completed in {stopwatch.ElapsedMilliseconds}ms: {successCount} succeeded, {failCount} failed");
_logger.LogInformation("Table initialization completed in {ElapsedMs}ms: {Success} succeeded, {Failed} failed",
stopwatch.ElapsedMilliseconds, successCount, failCount);
// CPE mapping seed is imported via Admin UI "Re-Seed Mappings" button
// (pulls from GitHub, not embedded resource). No auto-import at startup.
@@ -11,11 +11,39 @@ const msalInstance = new PublicClientApplication(msalConfig);
// Track MSAL initialization state so components can wait for it.
let msalReady = false;
// Prefetch: store auth/me result fetched during MSAL init so fetchUserInfo
// can use it immediately without an extra network round-trip.
// Runs as a fire-and-forget side-effect — MUST NOT block msalInitPromise,
// otherwise a cold backend would keep the UI on a white screen.
let prefetchedAuthMe: Record<string, unknown> | null = null;
const msalInitPromise = msalInstance
.initialize()
.then(() => msalInstance.handleRedirectPromise())
.then(() => {
msalReady = true;
// Fire-and-forget: prefetch auth/me while React is still mounting.
// Not awaited — if it finishes before fetchUserInfo runs, great;
// if not, fetchUserInfo does its own fetch as before.
const accounts = msalInstance.getAllAccounts();
if (accounts.length > 0) {
msalInstance.acquireTokenSilent({
scopes: apiRequest.scopes,
account: accounts[0],
}).then(async (tokenResponse) => {
const res = await fetch(api.auth.me(), {
headers: { 'Authorization': `Bearer ${tokenResponse.accessToken}` },
signal: AbortSignal.timeout(8000),
});
if (res.ok) {
prefetchedAuthMe = await res.json();
}
}).catch(() => {
// Best-effort; fetchUserInfo will retry normally.
});
}
})
.catch((error) => {
console.error('[Auth] MSAL initialization/redirect error:', error);
@@ -111,6 +139,24 @@ function AuthProviderInternal({ children }: { children: React.ReactNode }) {
*/
const fetchUserInfo = useCallback(async (account: AccountInfo): Promise<UserInfo | null> => {
try {
// Use prefetched auth/me result if available (fetched during MSAL init).
// Consume it exactly once to avoid stale data on subsequent calls.
if (prefetchedAuthMe) {
const data = prefetchedAuthMe;
prefetchedAuthMe = null;
return {
displayName: (data.displayName as string) || account.name || '',
upn: (data.upn as string) || account.username || '',
tenantId: (data.tenantId as string) || account.tenantId || '',
objectId: (data.objectId as string) || account.homeAccountId || '',
isGlobalAdmin: (data.isGlobalAdmin as boolean) || false,
isTenantAdmin: (data.isTenantAdmin as boolean) || false,
role: (data.role as 'Admin' | 'Operator' | 'Viewer' | null) || null,
canManageBootstrapTokens: (data.canManageBootstrapTokens as boolean) || false,
hasMcpAccess: (data.hasMcpAccess as boolean) || false,
};
}
// Get access token for API
const tokenResponse = await instance.acquireTokenSilent({
scopes: apiRequest.scopes,
@@ -144,10 +144,13 @@ export function SignalRProvider({ children }: { children: React.ReactNode }) {
}
};
startConnection();
// Defer SignalR connection to avoid competing with critical API calls
// (auth/me, sessions, tenant config) during initial page load.
const deferTimer = setTimeout(startConnection, 2000);
// Cleanup only when provider unmounts (app closes)
return () => {
clearTimeout(deferTimer);
// Clear any pending retry timeout to prevent reconnection after unmount
if (retryTimeoutRef.current) {
clearTimeout(retryTimeoutRef.current);
File diff suppressed because one or more lines are too long