feat(application): add OAuth2 authorization flow commands and queries (GetGoogleAuthorizationUrl, CompleteGoogleAuthorization)

This commit is contained in:
2026-08-17 15:05:12 +02:00
parent 502ae5e07b
commit 6d6e876d94
2 changed files with 118 additions and 0 deletions

View File

@@ -0,0 +1,72 @@
#if NET
using DigitalData.MessagingService.Application.Common.Interfaces;
using DigitalData.MessagingService.Application.Common.Interfaces.Repositories;
using DigitalData.MessagingService.Domain.Entities;
using DigitalData.MessagingService.Domain.Exceptions;
using MediatR;
using Microsoft.Extensions.Logging;
namespace DigitalData.MessagingService.Application.OAuth2.Commands;
/// <summary>
/// Exchanges a Google OAuth2 authorization code for a refresh token
/// and persists it on the email account.
/// Call this from the OAuth2 callback endpoint after the user grants consent.
/// </summary>
public record CompleteOAuth2AuthorizationCommand : IRequest<CompleteOAuth2AuthorizationResult>
{
/// <summary>
/// ID of the email account being authorized.
/// </summary>
public required int AccountId { get; init; }
/// <summary>
/// The authorization code received from the OAuth2 provider callback.
/// </summary>
public required string Code { get; init; }
/// <summary>
/// The redirect URI used in the original authorization request.
/// </summary>
public required string RedirectUri { get; init; }
}
public record CompleteOAuth2AuthorizationResult
{
public required string Username { get; init; }
public required bool Success { get; init; }
public string? ErrorMessage { get; init; }
}
public class CompleteOAuth2AuthorizationCommandHandler(
IRepository<EmailAccount> Repo,
IOAuth2AuthorizationService AuthService,
ILogger<CompleteOAuth2AuthorizationCommandHandler> Logger) : IRequestHandler<CompleteOAuth2AuthorizationCommand, CompleteOAuth2AuthorizationResult>
{
public async Task<CompleteOAuth2AuthorizationResult> Handle(CompleteOAuth2AuthorizationCommand request, CancellationToken cancellationToken)
{
var account = await Repo.GetByIdAsync(request.AccountId, cancellationToken)
?? throw new NotFoundException($"No email account found with Id: {request.AccountId}.");
Logger.LogInformation("Exchanging OAuth2 authorization code for account '{Username}' (Id: {Id}).",
account.Username, account.Id);
var refreshToken = await AuthService.ExchangeCodeForRefreshTokenAsync(
account, request.Code, request.RedirectUri, cancellationToken);
// Set the refresh token directly on the tracked entity — no AutoMapper needed
account.OAuth2RefreshToken = refreshToken;
await Repo.UpdateSingleAsync(a => a.Id == account.Id, account, cancellationToken);
Logger.LogInformation("OAuth2 refresh token saved for account '{Username}' (Id: {Id}).",
account.Username, account.Id);
return new CompleteOAuth2AuthorizationResult
{
Username = account.Username,
Success = true
};
}
}
#endif

View File

@@ -0,0 +1,46 @@
#if NET
using DigitalData.MessagingService.Application.Common.Interfaces;
using DigitalData.MessagingService.Application.Common.Interfaces.Repositories;
using DigitalData.MessagingService.Domain.Entities;
using DigitalData.MessagingService.Domain.Enums;
using DigitalData.MessagingService.Domain.Exceptions;
using MediatR;
namespace DigitalData.MessagingService.Application.OAuth2.Queries;
/// <summary>
/// Returns the authorization URL the user must visit to grant OAuth2 access.
/// Supported for providers that require user-delegated access (e.g. Google).
/// </summary>
public record GetOAuth2AuthorizationUrlQuery : IRequest<string>
{
/// <summary>
/// ID of the email account to authorize.
/// </summary>
public required int AccountId { get; init; }
/// <summary>
/// The redirect URI registered with the OAuth2 provider.
/// Must exactly match the URI configured in the provider's developer console.
/// </summary>
public required string RedirectUri { get; init; }
}
public class GetOAuth2AuthorizationUrlQueryHandler(
IRepository<EmailAccount> Repo,
IOAuth2AuthorizationService AuthService) : IRequestHandler<GetOAuth2AuthorizationUrlQuery, string>
{
public async Task<string> Handle(GetOAuth2AuthorizationUrlQuery request, CancellationToken cancellationToken)
{
var account = await Repo.GetByIdAsync(request.AccountId, cancellationToken)
?? throw new NotFoundException($"No email account found with Id: {request.AccountId}.");
if (account.OAuth2Provider == OAuth2Provider.None)
throw new BadRequestException(
$"Account '{account.Username}' (Id: {account.Id}) has no OAuth2 provider configured. " +
$"Set OAuth2Provider to a supported value (e.g. Google).");
return AuthService.GetAuthorizationUrl(account, request.RedirectUri);
}
}
#endif