using System.Security.Claims; using JobTrackerApi.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace JobTrackerApi.Controllers; /// /// Outlook / Microsoft 365 connection lifecycle (connect, OAuth callback, status, disconnect). /// Mirrors the Gmail OAuth surface in . Message search/import runs /// through the provider-neutral IEmailProvider seam once a job's correspondence flow is /// generalised past Gmail; this controller only owns the Microsoft-specific connection lifecycle. /// [ApiController] [Route("api/microsoft-graph")] [Authorize] public sealed class MicrosoftGraphController : ControllerBase { private readonly IMicrosoftGraphOAuthService _graph; private readonly IConfiguration _cfg; public MicrosoftGraphController(IMicrosoftGraphOAuthService graph, IConfiguration cfg) { _graph = graph; _cfg = cfg; } public sealed record MicrosoftGraphConnectionStatusDto( bool Connected, string? MailAddress, DateTimeOffset? ConnectedAt, DateTimeOffset? LastSyncedAt, DateTimeOffset? LastSyncAttemptedAt, DateTimeOffset? LastSyncSucceededAt, string? LastSyncMode, string? LastSyncSource, string? LastSyncStatus, string? LastSyncError); [HttpGet("status")] public async Task> Status(CancellationToken cancellationToken) { var ownerUserId = GetRequiredOwnerUserId(); var connection = await _graph.GetConnectionAsync(ownerUserId, cancellationToken); return Ok(new MicrosoftGraphConnectionStatusDto( connection is not null, connection?.MailAddress, connection?.ConnectedAt, connection?.LastSyncedAt, connection?.LastSyncAttemptedAt, connection?.LastSyncSucceededAt, connection?.LastSyncMode, connection?.LastSyncSource, connection?.LastSyncStatus, connection?.LastSyncError)); } [HttpGet("connect-url")] public IActionResult ConnectUrl() { var ownerUserId = GetRequiredOwnerUserId(); var url = _graph.BuildAuthorizationUrl(ownerUserId, GetRedirectUri()); return Ok(new { url }); } [AllowAnonymous] [HttpGet("oauth/callback")] public async Task Callback([FromQuery] string? code, [FromQuery] string? state, [FromQuery] string? error, CancellationToken cancellationToken) { if (!string.IsNullOrWhiteSpace(error)) { return Content(BuildPopupHtml(false, $"Microsoft returned an error: {error}"), "text/html"); } if (string.IsNullOrWhiteSpace(code) || string.IsNullOrWhiteSpace(state)) { return Content(BuildPopupHtml(false, "Missing Microsoft OAuth code or state."), "text/html"); } var ownerUserId = _graph.ConsumeState(state); if (string.IsNullOrWhiteSpace(ownerUserId)) { return Content(BuildPopupHtml(false, "This Outlook connection request is no longer valid. Start the connection again."), "text/html"); } try { var result = await _graph.ExchangeCodeAsync(ownerUserId, code, GetRedirectUri(), cancellationToken); return Content(BuildPopupHtml(true, $"Connected Outlook: {result.MailAddress}"), "text/html"); } catch (Exception ex) { return Content(BuildPopupHtml(false, ex.Message), "text/html"); } } [HttpDelete("connection")] public async Task Disconnect(CancellationToken cancellationToken) { var ownerUserId = GetRequiredOwnerUserId(); await _graph.DisconnectAsync(ownerUserId, cancellationToken); return NoContent(); } private string GetRequiredOwnerUserId() { return User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub") ?? throw new InvalidOperationException("Authenticated user id is missing."); } private string GetRedirectUri() { var configured = (_cfg["Microsoft:RedirectUri"] ?? "").Trim(); if (!string.IsNullOrWhiteSpace(configured)) return configured; var publicBaseUrl = (_cfg["App:PublicBaseUrl"] ?? "").Trim().TrimEnd('/'); if (!string.IsNullOrWhiteSpace(publicBaseUrl)) { return $"{publicBaseUrl}/api/microsoft-graph/oauth/callback"; } return $"{Request.Scheme}://{Request.Host}/api/microsoft-graph/oauth/callback"; } private static string BuildPopupHtml(bool success, string message) { var escaped = System.Net.WebUtility.HtmlEncode(message); var status = success ? "connected" : "error"; var title = success ? "Outlook connected" : "Outlook connection failed"; var serializedMessage = System.Text.Json.JsonSerializer.Serialize(message); return $@" Outlook connection

{title}

{escaped}

You can close this window.

"; } }