Files
jobtrackingapp/JobTrackerApi/Controllers/MicrosoftGraphController.cs
T

142 lines
5.1 KiB
C#

using System.Security.Claims;
using JobTrackerApi.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace JobTrackerApi.Controllers;
/// <summary>
/// Outlook / Microsoft 365 connection lifecycle (connect, OAuth callback, status, disconnect).
/// Mirrors the Gmail OAuth surface in <see cref="GmailController"/>. Message search/import runs
/// through the provider-neutral <c>IEmailProvider</c> seam once a job's correspondence flow is
/// generalised past Gmail; this controller only owns the Microsoft-specific connection lifecycle.
/// </summary>
[ApiController]
[Route("api/microsoft-graph")]
[Authorize]
public sealed class MicrosoftGraphController : ControllerBase
{
private readonly IMicrosoftGraphOAuthService _graph;
private readonly ExternalOrigin _externalOrigin;
public MicrosoftGraphController(IMicrosoftGraphOAuthService graph, IConfiguration cfg, ExternalOrigin? externalOrigin = null)
{
_graph = graph;
_externalOrigin = externalOrigin ?? ExternalOrigin.FromConfiguration(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<ActionResult<MicrosoftGraphConnectionStatusDto>> 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<IActionResult> 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<IActionResult> 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()
{
return _externalOrigin.BuildPath("/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 $@"<!doctype html>
<html>
<head>
<meta charset=""utf-8"" />
<title>Outlook connection</title>
</head>
<body style=""font-family:Segoe UI,Arial,sans-serif;padding:24px;line-height:1.5;"">
<h2>{title}</h2>
<p>{escaped}</p>
<p>You can close this window.</p>
<script>
if (window.opener) {{
window.opener.postMessage({{ source: 'jobtracker-microsoft-oauth', status: '{status}', message: {serializedMessage} }}, '*');
}}
window.close();
</script>
</body>
</html>";
}
}