feat(application): add OAuth2 authorization flow commands and queries (GetGoogleAuthorizationUrl, CompleteGoogleAuthorization)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user