feat(api): add OAuth2Controller (Google authorize/callback), SyncController (on-demand trigger); register IHttpContextAccessor
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
using DigitalData.MessagingService.Application.OAuth2.Commands;
|
||||
using DigitalData.MessagingService.Application.OAuth2.Queries;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace DigitalData.MessagingService.API.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Manages the OAuth2 authorization code flow for email accounts.
|
||||
/// Use these endpoints to authorize Google accounts without manually
|
||||
/// obtaining refresh tokens via external tools.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class OAuth2Controller(IMediator mediator, IHttpContextAccessor httpContextAccessor) : ControllerBase
|
||||
{
|
||||
#region Google Authorization Flow
|
||||
|
||||
/// <summary>
|
||||
/// Step 1: Redirects the user to Google's consent screen for the specified email account.
|
||||
/// After consent, Google redirects to <c>/api/oauth2/google/callback</c> with an authorization code.
|
||||
/// </summary>
|
||||
/// <param name="accountId">ID of the email account to authorize.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>HTTP 302 redirect to Google consent screen.</returns>
|
||||
[HttpGet("google/authorize/{accountId:int}")]
|
||||
[ProducesResponseType(StatusCodes.Status302Found)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> AuthorizeGoogle(int accountId, CancellationToken cancellationToken)
|
||||
{
|
||||
var redirectUri = BuildCallbackUri();
|
||||
|
||||
var authUrl = await mediator.Send(new GetOAuth2AuthorizationUrlQuery
|
||||
{
|
||||
AccountId = accountId,
|
||||
RedirectUri = redirectUri
|
||||
}, cancellationToken);
|
||||
|
||||
return Redirect(authUrl);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Step 2: Google callback endpoint. Exchanges the authorization code for a refresh token
|
||||
/// and saves it to the email account. This endpoint is called automatically by Google
|
||||
/// after the user grants consent — do not call it directly.
|
||||
/// </summary>
|
||||
/// <param name="code">Authorization code provided by Google.</param>
|
||||
/// <param name="state">Account ID passed as state in the authorization request.</param>
|
||||
/// <param name="error">Error message if the user denied access.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>HTTP 200 on success, HTTP 400 if access was denied.</returns>
|
||||
[HttpGet("google/callback")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<IActionResult> GoogleCallback(
|
||||
[FromQuery] string? code,
|
||||
[FromQuery] string? state,
|
||||
[FromQuery] string? error,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(error))
|
||||
return BadRequest(new { Error = error, Message = "User denied access or an error occurred during Google OAuth2 authorization." });
|
||||
|
||||
if (string.IsNullOrWhiteSpace(code))
|
||||
return BadRequest(new { Error = "missing_code", Message = "Authorization code not received from Google." });
|
||||
|
||||
if (!int.TryParse(state, out var accountId))
|
||||
return BadRequest(new { Error = "invalid_state", Message = "Invalid state parameter — could not determine account ID." });
|
||||
|
||||
var redirectUri = BuildCallbackUri();
|
||||
|
||||
var result = await mediator.Send(new CompleteOAuth2AuthorizationCommand
|
||||
{
|
||||
AccountId = accountId,
|
||||
Code = code,
|
||||
RedirectUri = redirectUri
|
||||
}, cancellationToken);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
result.Success,
|
||||
result.Username,
|
||||
Message = $"Google OAuth2 authorization completed. Refresh token saved for account '{result.Username}'."
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private string BuildCallbackUri()
|
||||
{
|
||||
var request = httpContextAccessor.HttpContext!.Request;
|
||||
return $"{request.Scheme}://{request.Host}/api/oauth2/google/callback";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using DigitalData.MessagingService.Infrastructure.Services.Background;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace DigitalData.MessagingService.API.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Controls email synchronization operations.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class SyncController(IEnumerable<IHostedService> hostedServices) : ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Triggers an immediate email sync cycle for all configured accounts,
|
||||
/// skipping the remaining interval wait.
|
||||
/// If a sync is already in progress, the next cycle will start immediately after it completes.
|
||||
/// </summary>
|
||||
/// <returns>HTTP 202 Accepted.</returns>
|
||||
[HttpPost("trigger")]
|
||||
[ProducesResponseType(StatusCodes.Status202Accepted)]
|
||||
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
||||
public IActionResult TriggerSync()
|
||||
{
|
||||
var syncWorker = hostedServices.OfType<EmailSyncWorker>().FirstOrDefault();
|
||||
|
||||
if (syncWorker is null)
|
||||
return StatusCode(StatusCodes.Status503ServiceUnavailable,
|
||||
new { Message = "EmailSyncWorker is not running." });
|
||||
|
||||
syncWorker.TriggerSync();
|
||||
|
||||
return Accepted(new { Message = "Sync triggered. The next cycle will start immediately." });
|
||||
}
|
||||
}
|
||||
@@ -71,6 +71,9 @@ try
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen();
|
||||
|
||||
// Required by OAuth2Controller to build callback URIs
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
|
||||
// Register Serilog.UI with SQLite provider for web log viewer
|
||||
builder.Services.AddSerilogUi(options =>
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user