using System.Net; using System.Web; using AutopilotMonitor.Functions.Helpers; using AutopilotMonitor.Functions.Services; using AutopilotMonitor.Shared.DataAccess; using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.Functions.Worker.Http; using Microsoft.Extensions.Logging; namespace AutopilotMonitor.Functions.Functions.Progress; /// /// Dedicated Progress Portal endpoints. /// These routes are exempt from MemberAuthorizationMiddleware so any authenticated tenant user /// can monitor enrollment sessions without needing an Admin or Operator role. /// public class ProgressPortalFunction { private readonly ILogger _logger; private readonly ISessionRepository _sessionRepo; private readonly GlobalAdminService _globalAdminService; public ProgressPortalFunction( ILogger logger, ISessionRepository sessionRepo, GlobalAdminService globalAdminService) { _logger = logger; _sessionRepo = sessionRepo; _globalAdminService = globalAdminService; } /// /// GET /api/progress/sessions /// Returns sessions for the authenticated user's tenant. /// [Function("ProgressGetSessions")] public async Task GetSessions( [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "progress/sessions")] HttpRequestData req) { _logger.LogInformation("ProgressGetSessions processing request"); try { // Authentication + AuthenticatedUser authorization enforced by PolicyEnforcementMiddleware var tenantId = TenantHelper.GetTenantId(req); _logger.LogInformation("ProgressGetSessions: Fetching sessions for tenant {TenantId}", tenantId); var page = await _sessionRepo.GetSessionsAsync(tenantId, maxResults: 100); var response = req.CreateResponse(HttpStatusCode.OK); await response.WriteAsJsonAsync(new { success = true, count = page.Sessions.Count, sessions = page.Sessions }); return response; } catch (Exception ex) { _logger.LogError(ex, "Error in ProgressGetSessions"); var errorResponse = req.CreateResponse(HttpStatusCode.InternalServerError); await errorResponse.WriteAsJsonAsync(new { success = false, message = "Internal server error", count = 0, sessions = Array.Empty() }); return errorResponse; } } /// /// GET /api/progress/sessions/{sessionId}/events /// Returns events for a specific session. Cross-tenant access only for Global Admins. /// [Function("ProgressGetSessionEvents")] public async Task GetSessionEvents( [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "progress/sessions/{sessionId}/events")] HttpRequestData req, string sessionId) { if (string.IsNullOrEmpty(sessionId)) { var badRequestResponse = req.CreateResponse(HttpStatusCode.BadRequest); await badRequestResponse.WriteAsJsonAsync(new { success = false, message = "SessionId is required" }); return badRequestResponse; } var sessionPrefix = $"[Session: {sessionId.Substring(0, Math.Min(8, sessionId.Length))}]"; _logger.LogInformation("{SessionPrefix} ProgressGetSessionEvents: Fetching events", sessionPrefix); try { // Authentication + AuthenticatedUser authorization enforced by PolicyEnforcementMiddleware var userTenantId = TenantHelper.GetTenantId(req); var userIdentifier = TenantHelper.GetUserIdentifier(req); var query = HttpUtility.ParseQueryString(req.Url.Query); var requestedTenantId = query["tenantId"]; if (string.IsNullOrEmpty(requestedTenantId)) { var badRequest = req.CreateResponse(HttpStatusCode.BadRequest); await badRequest.WriteAsJsonAsync(new { success = false, message = "tenantId query parameter is required", sessionId = sessionId, count = 0, events = Array.Empty() }); return badRequest; } // Cross-tenant access only for Global Admins if (requestedTenantId != userTenantId) { var isGlobalAdmin = await _globalAdminService.IsGlobalAdminAsync(userIdentifier); if (!isGlobalAdmin) { var forbiddenResponse = req.CreateResponse(HttpStatusCode.Forbidden); await forbiddenResponse.WriteAsJsonAsync(new { success = false, message = "Access denied. You can only view events for sessions in your own tenant.", sessionId = sessionId, count = 0, events = Array.Empty() }); return forbiddenResponse; } _logger.LogInformation("{SessionPrefix} Global Admin {User} accessing cross-tenant progress events (tenant: {TenantId})", sessionPrefix, userIdentifier, requestedTenantId); } var events = await _sessionRepo.GetSessionEventsAsync(requestedTenantId, sessionId); var response = req.CreateResponse(HttpStatusCode.OK); await response.WriteAsJsonAsync(new { success = true, sessionId = sessionId, count = events.Count, events = events }); return response; } catch (Exception ex) { _logger.LogError(ex, "Error in ProgressGetSessionEvents for session {SessionId}", sessionId); var errorResponse = req.CreateResponse(HttpStatusCode.InternalServerError); await errorResponse.WriteAsJsonAsync(new { success = false, message = "Internal server error", sessionId = sessionId, count = 0, events = Array.Empty() }); return errorResponse; } } }