feat(api,core,infra): refactor endpoint routing and add institution account discovery
- Move login and refresh endpoints under a unified /auth route group
- Relocate institution connection logic from /accounts to /institutions
- Implement GET /institutions/{id}/available to fetch real-time Plaid
accounts
- Introduce HttpRequestBuilder utility to streamline integration testing
- Enhance PlaidService with GetAccountsAsync to support account fetching
- Clean up global usings and project structure for better domain
isolation
This commit is contained in:
@@ -10,7 +10,6 @@ internal static class AccountsExtensions
|
||||
.RequireAuthorization();
|
||||
|
||||
accountsGroup.MapAddEndpoint();
|
||||
accountsGroup.MapConnectEndpoint();
|
||||
|
||||
return accountsGroup;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
using FiscalOS.Core.Accounts;
|
||||
|
||||
namespace FiscalOS.API.Accounts.Add;
|
||||
|
||||
internal static class Endpoint
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
namespace FiscalOS.API.Accounts.Add;
|
||||
|
||||
public record Request : IValidatableObject
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace FiscalOS.API.Auth;
|
||||
|
||||
internal static class AuthExtensions
|
||||
{
|
||||
private const string RouteGroupPrefix = "/auth";
|
||||
|
||||
public static RouteGroupBuilder MapAuthEndpoints(this WebApplication app)
|
||||
{
|
||||
var authGroup = app.MapGroup(RouteGroupPrefix);
|
||||
|
||||
authGroup.MapLoginEndpoint();
|
||||
|
||||
authGroup.MapRefreshEndpoint()
|
||||
.RequireAuthorization(Schemes.AllowExpiredTokens);
|
||||
|
||||
return authGroup;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
namespace FiscalOS.API.Login;
|
||||
namespace FiscalOS.API.Auth.Login;
|
||||
|
||||
internal static class Endpoint
|
||||
{
|
||||
private const string Route = "/login";
|
||||
|
||||
public static RouteHandlerBuilder MapLoginEndpoint(this WebApplication app)
|
||||
public static RouteHandlerBuilder MapLoginEndpoint(this RouteGroupBuilder groupBuilder)
|
||||
{
|
||||
return app.MapPost(Route, HandleAsync);
|
||||
return groupBuilder.MapPost(Route, HandleAsync);
|
||||
}
|
||||
|
||||
private static async Task<IResult> HandleAsync(
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace FiscalOS.API.Login;
|
||||
namespace FiscalOS.API.Auth.Login;
|
||||
|
||||
public record Request : IValidatableObject
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace FiscalOS.API.Login;
|
||||
namespace FiscalOS.API.Auth.Login;
|
||||
|
||||
internal sealed record Response
|
||||
{
|
||||
@@ -1,12 +1,12 @@
|
||||
namespace FiscalOS.API.Refresh;
|
||||
namespace FiscalOS.API.Auth.Refresh;
|
||||
|
||||
internal static class Endpoint
|
||||
{
|
||||
private const string Route = "/refresh";
|
||||
|
||||
public static RouteHandlerBuilder MapRefreshEndpoint(this WebApplication app)
|
||||
public static RouteHandlerBuilder MapRefreshEndpoint(this RouteGroupBuilder groupBuilder)
|
||||
{
|
||||
return app.MapPost(Route, HandleAsync);
|
||||
return groupBuilder.MapPost(Route, HandleAsync);
|
||||
}
|
||||
|
||||
private static async Task<IResult> HandleAsync(
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace FiscalOS.API.Refresh;
|
||||
namespace FiscalOS.API.Auth.Refresh;
|
||||
|
||||
internal sealed record Response
|
||||
{
|
||||
@@ -1,5 +1,3 @@
|
||||
using FiscalOS.Core.Identity;
|
||||
|
||||
namespace FiscalOS.API.Http;
|
||||
|
||||
internal static class HttpContextExtensions
|
||||
|
||||
+1
-3
@@ -1,6 +1,4 @@
|
||||
using FiscalOS.Core.Accounts;
|
||||
|
||||
namespace FiscalOS.API.Accounts.Connect;
|
||||
namespace FiscalOS.API.Institutions.Connect;
|
||||
|
||||
internal static class Endpoint
|
||||
{
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace FiscalOS.API.Accounts.Connect;
|
||||
namespace FiscalOS.API.Institutions.Connect;
|
||||
|
||||
public record Request : IValidatableObject
|
||||
{
|
||||
@@ -0,0 +1,50 @@
|
||||
namespace FiscalOS.API.Institutions.GetAvailable;
|
||||
|
||||
internal static class Endpoint
|
||||
{
|
||||
private const string Route = "/{id}/available";
|
||||
|
||||
public static RouteHandlerBuilder MapGetAvailableEndpoint(this RouteGroupBuilder groupBuilder)
|
||||
{
|
||||
return groupBuilder.MapGet(Route, HandleAsync);
|
||||
}
|
||||
|
||||
private static async Task<IResult> HandleAsync(
|
||||
HttpContext httpContext,
|
||||
[FromRoute] Guid id,
|
||||
[FromServices] AppDbContext appDbContext,
|
||||
[FromServices] PlaidService plaidService,
|
||||
[FromServices] IEncryptor encryptor,
|
||||
CancellationToken ct
|
||||
)
|
||||
{
|
||||
var userId = httpContext.GetUserId();
|
||||
|
||||
var user = await appDbContext.Users
|
||||
.Include(u => u.Institutions.Where(i => i.Id == id))
|
||||
.ThenInclude(i => i.Metadata)
|
||||
.SingleOrDefaultAsync(u => u.Id == userId, ct);
|
||||
|
||||
if (user is null)
|
||||
{
|
||||
return Results.Unauthorized();
|
||||
}
|
||||
|
||||
if (user.Institutions.Any() is false)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
var institution = user.Institutions.First();
|
||||
|
||||
if (institution.Metadata is not PlaidMetadata plaidMetadata)
|
||||
{
|
||||
return Results.BadRequest();
|
||||
}
|
||||
|
||||
var accessToken = await encryptor.DecryptAsyncFor(user, plaidMetadata.EncryptedAccessToken, ct);
|
||||
var accounts = await plaidService.GetAccountsAsync(accessToken);
|
||||
|
||||
return Results.Ok(accounts);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace FiscalOS.API.Institutions;
|
||||
|
||||
internal static class InstitutionsExtensions
|
||||
{
|
||||
private const string RouteGroupPrefix = "/institutions";
|
||||
|
||||
public static RouteGroupBuilder MapInstitutionsEndpoints(this WebApplication app)
|
||||
{
|
||||
var institutionsGroup = app.MapGroup(RouteGroupPrefix)
|
||||
.RequireAuthorization();
|
||||
|
||||
institutionsGroup.MapConnectEndpoint();
|
||||
institutionsGroup.MapGetAvailableEndpoint();
|
||||
|
||||
return institutionsGroup;
|
||||
}
|
||||
}
|
||||
@@ -32,11 +32,8 @@ app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
app.UseStatusCodePages();
|
||||
|
||||
app.MapLoginEndpoint();
|
||||
|
||||
app.MapRefreshEndpoint()
|
||||
.RequireAuthorization(Schemes.AllowExpiredTokens);
|
||||
|
||||
app.MapAuthEndpoints();
|
||||
app.MapAccountsEndpoints();
|
||||
app.MapInstitutionsEndpoints();
|
||||
|
||||
app.Run();
|
||||
@@ -2,12 +2,17 @@ global using System.ComponentModel.DataAnnotations;
|
||||
global using System.Security.Claims;
|
||||
global using System.Text.Json.Serialization;
|
||||
|
||||
global using FiscalOS.Core.Identity;
|
||||
global using FiscalOS.API.Accounts;
|
||||
global using FiscalOS.API.Accounts.Add;
|
||||
global using FiscalOS.API.Accounts.Connect;
|
||||
global using FiscalOS.API.Auth;
|
||||
global using FiscalOS.API.Auth.Login;
|
||||
global using FiscalOS.API.Auth.Refresh;
|
||||
global using FiscalOS.API.Http;
|
||||
global using FiscalOS.API.Login;
|
||||
global using FiscalOS.API.Refresh;
|
||||
global using FiscalOS.API.Institutions;
|
||||
global using FiscalOS.API.Institutions.Connect;
|
||||
global using FiscalOS.API.Institutions.GetAvailable;
|
||||
global using FiscalOS.Core.Accounts;
|
||||
global using FiscalOS.Core.Authentication;
|
||||
global using FiscalOS.Core.Security;
|
||||
global using FiscalOS.Infra.Accounts.Plaid;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
namespace FiscalOS.Infra.Accounts.Plaid;
|
||||
|
||||
public sealed class PlaidService
|
||||
@@ -30,6 +31,21 @@ public sealed class PlaidService
|
||||
return (ptr.ItemId, ptr.AccessToken);
|
||||
}
|
||||
|
||||
public async Task<List<Going.Plaid.Entity.Account>> GetAccountsAsync(string accessToken)
|
||||
{
|
||||
var ar = await _client.AccountsGetAsync(new()
|
||||
{
|
||||
AccessToken = accessToken,
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
if (ar.IsSuccessStatusCode is false)
|
||||
{
|
||||
throw new PlaidException("Unable to retrieve accounts");
|
||||
}
|
||||
|
||||
return [.. ar.Accounts];
|
||||
}
|
||||
|
||||
public async Task<ItemWithConsentFields> GetItemAsync(string accessToken)
|
||||
{
|
||||
var ar = await _client.ItemGetAsync(new()
|
||||
@@ -44,4 +60,4 @@ public sealed class PlaidService
|
||||
|
||||
return ar.Item;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user