using Books.Api.Banking; using Books.Api.Commands.BankConnections; using Books.Api.Domain.BankConnections; using EventFlow; using EventFlow.Aggregates.ExecutionResults; using Microsoft.AspNetCore.Mvc; namespace Books.Api.Controllers; [ApiController] [Route("api/banking")] public class BankingController : ControllerBase { private readonly ICommandBus _commandBus; private readonly IEnableBankingClient _bankingClient; private readonly ILogger _logger; private readonly IConfiguration _configuration; public BankingController( ICommandBus commandBus, IEnableBankingClient bankingClient, ILogger logger, IConfiguration configuration) { _commandBus = commandBus; _bankingClient = bankingClient; _logger = logger; _configuration = configuration; } /// /// OAuth callback from Enable Banking after user authorizes bank connection. /// [HttpGet("callback")] public async Task Callback( [FromQuery] string? code, [FromQuery] string? state, [FromQuery] string? error, [FromQuery] string? error_description, CancellationToken ct) { var frontendBaseUrl = _configuration["Frontend:BaseUrl"] ?? "http://localhost:3000"; var redirectUrl = $"{frontendBaseUrl}/indstillinger?tab=bankAccounts"; // Handle error from bank if (!string.IsNullOrEmpty(error)) { _logger.LogWarning("Bank authorization failed: {Error} - {Description}", error, error_description); return Redirect($"{redirectUrl}&error={Uri.EscapeDataString(error_description ?? error)}"); } // Validate required parameters if (string.IsNullOrEmpty(code) || string.IsNullOrEmpty(state)) { _logger.LogWarning("Missing code or state in callback"); return Redirect($"{redirectUrl}&error=missing_parameters"); } try { // TODO: Add proper CSRF/state validation. Currently the state parameter // is used as the connection ID, but it should also include a CSRF token // that is validated against the user session to prevent cross-site request // forgery attacks on the OAuth callback. // State contains the connection ID (set during StartBankConnection) var connectionId = state; // Get PSU headers from HttpContext (required by Enable Banking API) var psuIpAddress = HttpContext.Connection.RemoteIpAddress?.ToString(); var psuUserAgent = Request.Headers.UserAgent.ToString(); // Exchange authorization code for session var session = await _bankingClient.CreateSessionAsync(code, psuIpAddress, psuUserAgent, ct); _logger.LogInformation( "Bank session created: {SessionId}, Bank: {Bank}, Accounts: {AccountCount}", session.SessionId, session.AspspName, session.Accounts.Count); // Complete the bank connection var command = new EstablishBankConnectionCommand( BankConnectionId.With(connectionId), session.SessionId, session.ValidUntil, session.Accounts.Select(a => new BankAccountInfo( a.AccountId, a.Iban, a.Currency, a.AccountName)).ToList()); var result = await _commandBus.PublishAsync(command, ct); if (result is FailedExecutionResult failed) { _logger.LogError("Failed to complete bank connection: {Errors}", string.Join(", ", failed.Errors)); return Redirect($"{redirectUrl}&error=completion_failed"); } _logger.LogInformation("Bank connection {ConnectionId} completed successfully", connectionId); return Redirect($"{redirectUrl}&success=true"); } catch (Exception ex) { _logger.LogError(ex, "Error completing bank connection"); return Redirect($"{redirectUrl}&error=internal_error"); } } }