diff --git a/src/core/DigitalData.MessagingService.Application/OAuth2/Commands/CompleteOAuth2AuthorizationCommand.cs b/src/core/DigitalData.MessagingService.Application/OAuth2/Commands/CompleteOAuth2AuthorizationCommand.cs new file mode 100644 index 0000000..f1ac0cb --- /dev/null +++ b/src/core/DigitalData.MessagingService.Application/OAuth2/Commands/CompleteOAuth2AuthorizationCommand.cs @@ -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; + +/// +/// 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. +/// +public record CompleteOAuth2AuthorizationCommand : IRequest +{ + /// + /// ID of the email account being authorized. + /// + public required int AccountId { get; init; } + + /// + /// The authorization code received from the OAuth2 provider callback. + /// + public required string Code { get; init; } + + /// + /// The redirect URI used in the original authorization request. + /// + 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 Repo, + IOAuth2AuthorizationService AuthService, + ILogger Logger) : IRequestHandler +{ + public async Task 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 + diff --git a/src/core/DigitalData.MessagingService.Application/OAuth2/Queries/GetOAuth2AuthorizationUrlQuery.cs b/src/core/DigitalData.MessagingService.Application/OAuth2/Queries/GetOAuth2AuthorizationUrlQuery.cs new file mode 100644 index 0000000..c07d760 --- /dev/null +++ b/src/core/DigitalData.MessagingService.Application/OAuth2/Queries/GetOAuth2AuthorizationUrlQuery.cs @@ -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; + +/// +/// Returns the authorization URL the user must visit to grant OAuth2 access. +/// Supported for providers that require user-delegated access (e.g. Google). +/// +public record GetOAuth2AuthorizationUrlQuery : IRequest +{ + /// + /// ID of the email account to authorize. + /// + public required int AccountId { get; init; } + + /// + /// The redirect URI registered with the OAuth2 provider. + /// Must exactly match the URI configured in the provider's developer console. + /// + public required string RedirectUri { get; init; } +} + +public class GetOAuth2AuthorizationUrlQueryHandler( + IRepository Repo, + IOAuth2AuthorizationService AuthService) : IRequestHandler +{ + public async Task 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