Skip to content

Commit

Permalink
Fixed the routing of BEFFE APIs. To restrict them to the prefix /api.
Browse files Browse the repository at this point in the history
  • Loading branch information
jezzsantos committed Sep 28, 2024
1 parent b05d1c9 commit 90b19b8
Show file tree
Hide file tree
Showing 21 changed files with 50 additions and 43 deletions.
4 changes: 2 additions & 2 deletions docs/design-principles/0080-ports-and-adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,15 +264,15 @@ For example, the `BaseUrl` configuration setting in the `appsettings.json` file
In the `TestingStubApiHost` project, in the `Api` folder:

1. Create a class that derives from `StubApiBase` (e.g., `StubVendorApi`).
2. Consider applying a `WebServiceAttribute` to define a prefix for this API (e.g. `[WebService("/vendor")]`) to separate this API from the other vendors that will also be hosted at `https://localhost:5656`.
2. Consider applying a `BaseApiFromAttribute` to define a prefix for this API (e.g. `[BaseApiFrom("/vendor")]`) to separate this API from the other vendors that will also be hosted at `https://localhost:5656`.
3. Implement the HTTP endpoints that your adapter uses, and provide empty or default responses that your adapter expects to receive. (same as the way we implement any endpoints)
4. The Request types, Response types (and all complex data types that are used in the request and response) should all be defined in the `Infrastructure.Web.Api.Operations.Shared` project in a subfolder of the `3rdParties` folder named after the vendor (i.e., `3rdParties/Vendor`). These types follow the same patterns as requests and responses for all other API operations in the codebase. Except that these ones may use additional JSON attributes to match the real 3rd party APIs. (e.g., `JsonPropertyName` and `JsonConverter` attributes).
5. Make sure that you trace out (using the `IRecorder`) each and every request to your Stub API (follow other examples) so that you can visually track when the API is called (by your adapter) in local testing. You can see this output in the console output for the `TestingStubApiHost` project.

For example,

```csharp
[WebService("/vendor")]
[BaseApiFrom("/vendor")]
public class StubVendorApi : StubApiBase
{
public StubVendorApi(IRecorder recorder, IConfigurationSettings settings) : base(recorder, settings)
Expand Down
21 changes: 15 additions & 6 deletions docs/design-principles/0110-back-end-for-front-end.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@ In many contexts of evolving SaaS products, a BEFFE can act as an [Anti-Corrupti
The `WebsiteHost` project is a BEFFE. It performs these tasks:

1. It serves up the JS app (as static files).
2. It serves up other HTML, CSS, JS assets (as dynamic assets).
3. It implements a Reverse Proxy in order to forward JSON requests to Backend APIs
4. It implements its own Authentication API endpoints so that [HTTPOnly, secure] cookies can be used between the BEFFE and JS app to avoid storing any secrets in the browser.
5. It necessarily protects against CSRF attacks (since it uses cookies).
6. It provides API endpoints for common services, like: Recording (diagnostics, usages, crash reports, etc), Feature Flags, etc.
7. It provides a platform to add dedicated API endpoints tailored for the JS app that can be easily added as required.
2. It serves up other HTML, CSS, JS assets (as dynamic assets), using MVC (via Controllers)
3. It implements a Reverse Proxy in order to forward JSON API requests to Backend APIs
4. It implements its own Authentication API endpoints so that [HTTPOnly, secure] cookies can be used between the BEFFE and JS app, for security purposes, to avoid storing any tokens/secrets in the browser (anywhere).
5. It necessarily protects against CSRF attacks (since it uses cookies to store authorization tokens).
6. It provides other API endpoints for common services, like: Health monitoring, Recording (diagnostics, usages, crash reports, etc), Feature Flags, etc. These API's are available at `/api`.
7. It provides a platform to add further custom dedicated API endpoints, that you might provide yourself to the JS app. These might be necessary to avoid performing N+1 operations in the browser, versus doing them in the BEFFE (in teh cloud), which is far more efficient in latency.
8. It provides the opportunity to deploy (and scale) the web application separately from backend APIs.

### Reverse Proxy
Expand Down Expand Up @@ -349,3 +349,12 @@ The BEFFE has been designed to be extended to include additional (domain-specifi

These endpoints can simply be added in the same way API endpoints are added to any host. The Reverse Proxy will detect requests to these endpoints and route them directly to the BEFFE API to process first.

To create a new API, define a class in custom subfolder of the `Api` folder of the `WebsiteHost` project.

> This custom folder should be named after the subdomain that the API pertains to.
Next, like all other APIs in this codebase, create a class that derives from: `IWebApiService`.

One last necessary detail. All BEFFE API classes should apply the `[BaseApiFrom("/api")]` attribute to the class.

> Note: the `BaseApiFromAttribute` is necessary, so that your custom API is available to the JS app correctly somwhere under the `/api` prefix. Otherwise, it will be available under the root of the BEFFE `/`. This API then may clash with the routes of the page views that the JS App handles client-side, which should be avoided.
4 changes: 2 additions & 2 deletions docs/how-to-guides/100-build-adapter-third-party.md
Original file line number Diff line number Diff line change
Expand Up @@ -321,9 +321,9 @@ You will also need to be able to control whether your adapter talks to your stub

You will build your stub API in the `TestingStubApiHost` project in the `Api` folder.

You will need to add a `sealed class` that derives from `StubApiBase`, and that applies the `[WebService("/nameofintegration")]` attribute to it.
You will need to add a `sealed class` that derives from `StubApiBase`, and that applies the `[BaseApiFrom("/vendor")]` attribute to it.

> Note: the `[WebService]` attribute is required so that you can partition all inbound HTTP requests to the stub API they belong to, in cases where there are clashes with multiple vendors base URLs.
> Note: the `[BaseApiFrom]` attribute is required so that you can partition all inbound HTTP requests to the stub API they belong to, in cases where there are clashes with multiple vendors base URLs.
Next, and before you forget. Edit the value of the base URL setting that you might have put in the `appsettings.json` file of the `ApiHost` project, where you have registered your adapter using DI. Then, change the base URL value to point to your local stub API, so that your adapter will talk to your stub API in local development and debugging.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
namespace Infrastructure.Web.Api.Interfaces;

/// <summary>
/// Provides a declarative way to define a web API service
/// Provides a declarative way to define the base path of an API
/// </summary>
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public class WebServiceAttribute : Attribute
public class BaseApiFromAttribute : Attribute
{
public WebServiceAttribute(
public BaseApiFromAttribute(
#if !NETSTANDARD2_0
[StringSyntax("Route")]
#endif
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public async Task WhenInvokeAsyncAndIsAnAPICall_ThenForwardsToBackend()
Request =
{
Method = HttpMethods.Post,
PathBase = new PathString("/api/apath")
Path = new PathString("/api/apath")
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,6 @@ public static void AddBEFFE(this WebApplication builder,
return;
}

// Note: must be registered before CORS since it calls app.UsesRouting()
middlewares.Add(new MiddlewareRegistration(30,
app => { app.UsePathBase(new PathString(WebConstants.BackEndForFrontEndBasePath)); },
"Pipeline: Website API is enabled: Route -> {Route}", WebConstants.BackEndForFrontEndBasePath));
middlewares.Add(new MiddlewareRegistration(35, app =>
{
if (!app.Environment.IsDevelopment())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ private static bool IsBeffeApiRequest(HttpContext context)

private static bool IsApiRequest(HttpRequest request)
{
return request.PathBase.ToString().StartsWith(WebConstants.BackEndForFrontEndBasePath);
return request.Path.ToString().StartsWith(WebConstants.BackEndForFrontEndBasePath);
}

private async Task ForwardMessageToBackendAsync(HttpContext context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ public HealthCheckApiSpec(WebApiSetup<Program> setup) : base(setup)
[Fact]
public async Task WhenCheck_ThenStatusOK()
{
var result = await Api.GetAsync(new HealthCheckRequest());
var result = await HttpApi.GetAsync(new HealthCheckRequest().MakeApiRoute());

result.Content.Value.Name.Should().Be("WebsiteHost");
result.Content.Value.Status.Should().Be("OK");
var content = await result.Content.ReadAsStringAsync();
content.Should().Be("{\"name\":\"WebsiteHost\",\"status\":\"OK\"}");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,9 @@ public static string MakeApiRoute(this IWebRequest request)
return $"{WebConstants.BackEndForFrontEndBasePath}{request.GetRequestInfo().Route}";
}

public static async Task<string> RegisterPersonUserFromBrowserAsync(this IHttpClient websiteClient,
public static async Task RegisterPersonUserFromBrowserAsync(this IHttpClient websiteClient,
JsonSerializerOptions jsonOptions, CSRFMiddleware.ICSRFService csrfService,
string emailAddress,
string password)
string emailAddress, string password)
{
var registrationRequest = new RegisterPersonPasswordRequest
{
Expand Down Expand Up @@ -124,7 +123,6 @@ public static async Task<string> RegisterPersonUserFromBrowserAsync(this IHttpCl
await websiteClient.PostAsync(confirmationUrl, JsonContent.Create(confirmationRequest),
(msg, cookies) => msg.WithCSRF(cookies, csrfService));
#endif
return userId;
}

public static void WithCSRF(this HttpRequestMessage message, CookieContainer cookies,
Expand Down
2 changes: 1 addition & 1 deletion src/TestingStubApiHost/Api/StubFlagsmithApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

namespace TestingStubApiHost.Api;

[WebService("/flagsmith")]
[BaseApiFrom("/flagsmith")]
public sealed class StubFlagsmithApi : StubApiBase
{
private static readonly List<FlagsmithFlag> Flags = GetAllFlags();
Expand Down
2 changes: 1 addition & 1 deletion src/TestingStubApiHost/Api/StubGravatarApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

namespace TestingStubApiHost.Api;

[WebService("/gravatar")]
[BaseApiFrom("/gravatar")]
public sealed class StubGravatarApi : StubApiBase
{
public StubGravatarApi(IRecorder recorder, IConfigurationSettings settings) : base(recorder, settings)
Expand Down
2 changes: 1 addition & 1 deletion src/TestingStubApiHost/Api/StubMailgunApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

namespace TestingStubApiHost.Api;

[WebService("/mailgun")]
[BaseApiFrom("/mailgun")]
public class StubMailgunApi : StubApiBase
{
private readonly IServiceClient _serviceClient;
Expand Down
2 changes: 1 addition & 1 deletion src/TestingStubApiHost/Api/StubUserPilotApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

namespace TestingStubApiHost.Api;

[WebService("/userpilot")]
[BaseApiFrom("/userpilot")]
public class StubUserPilotApi : StubApiBase
{
public StubUserPilotApi(IRecorder recorder, IConfigurationSettings settings) : base(recorder, settings)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1179,7 +1179,7 @@ public class AMethod_ARequest_Handler : global::MediatR.IRequestHandler<global::
}

[Fact]
public void WhenDefinesAWebServiceAttribute_ThenGenerates()
public void WhenDefinesABaseApiFromAttribute_ThenGenerates()
{
var compilation = CreateCompilation("""
using System;
Expand All @@ -1196,7 +1196,7 @@ public class AResponse : IWebResponse
public class ARequest : WebRequest<ARequest, AResponse>
{
}
[WebService("aprefix")]
[BaseApiFrom("aprefix")]
public class AServiceClass : IWebApiService
{
public async Task<string> AMethod(ARequest request, CancellationToken cancellationToken)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -534,7 +534,7 @@ public string AMethod(ARequest request)
}

[Fact]
public void WhenVisitNamedTypeAndClassHasWebServiceAttribute_ThenCreatesRegistration()
public void WhenVisitNamedTypeAndClassHasBaseApiFromAttribute_ThenCreatesRegistration()
{
var compilation = CreateCompilation("""
using System;
Expand All @@ -549,7 +549,7 @@ public class AResponse : IWebResponse
public class ARequest : WebRequest<ARequest, AResponse>
{
}
[Infrastructure.Web.Api.Interfaces.WebServiceAttribute("aprefix")]
[Infrastructure.Web.Api.Interfaces.BaseApiFromAttribute("aprefix")]
public class AServiceClass : Infrastructure.Web.Api.Interfaces.IWebApiService
{
public string AMethod(ARequest request)
Expand Down
4 changes: 2 additions & 2 deletions src/Tools.Generators.Web.Api/Tools.Generators.Web.Api.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@
<Compile Include="..\Infrastructure.Web.Api.Interfaces\EmptyResponse.cs">
<Link>Reference\Infrastructure.Web.Api.Interfaces\EmptyResponse.cs</Link>
</Compile>
<Compile Include="..\Infrastructure.Web.Api.Interfaces\WebServiceAttribute.cs">
<Link>Reference\Infrastructure.Web.Api.Interfaces\WebServiceAttribute.cs</Link>
<Compile Include="..\Infrastructure.Web.Api.Interfaces\BaseApiFromAttribute.cs">
<Link>Reference\Infrastructure.Web.Api.Interfaces\BaseApiFromAttribute.cs</Link>
</Compile>
<Compile Include="..\Infrastructure.Web.Api.Interfaces\RouteAttribute.cs">
<Link>Reference\Infrastructure.Web.Api.Interfaces\RouteAttribute.cs</Link>
Expand Down
14 changes: 7 additions & 7 deletions src/Tools.Generators.Web.Api/WebApiAssemblyVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public class WebApiAssemblyVisitor : SymbolVisitor
private readonly INamedTypeSymbol _voidSymbol;
private readonly INamedTypeSymbol _webRequestInterfaceSymbol;
private readonly INamedTypeSymbol _webRequestResponseInterfaceSymbol;
private readonly INamedTypeSymbol _webserviceAttributeSymbol;
private readonly INamedTypeSymbol _baseApiAttributeSymbol;

public WebApiAssemblyVisitor(CancellationToken cancellationToken, Compilation compilation)
{
Expand All @@ -46,7 +46,7 @@ public WebApiAssemblyVisitor(CancellationToken cancellationToken, Compilation co
_webRequestInterfaceSymbol = compilation.GetTypeByMetadataName(typeof(IWebRequest).FullName!)!;
_tenantedWebRequestInterfaceSymbol = compilation.GetTypeByMetadataName(typeof(ITenantedRequest).FullName!)!;
_webRequestResponseInterfaceSymbol = compilation.GetTypeByMetadataName(typeof(IWebRequest<>).FullName!)!;
_webserviceAttributeSymbol = compilation.GetTypeByMetadataName(typeof(WebServiceAttribute).FullName!)!;
_baseApiAttributeSymbol = compilation.GetTypeByMetadataName(typeof(BaseApiFromAttribute).FullName!)!;
_routeAttributeSymbol = compilation.GetTypeByMetadataName(typeof(RouteAttribute).FullName!)!;
_authorizeAttributeSymbol = compilation.GetTypeByMetadataName(typeof(AuthorizeAttribute).FullName!)!;
_authorizeAttributeRolesSymbol = compilation.GetTypeByMetadataName(typeof(Roles).FullName!)!;
Expand Down Expand Up @@ -226,7 +226,7 @@ private void AddRegistration(INamedTypeSymbol symbol)

string? GetBasePath()
{
if (!HasWebServiceAttribute(symbol, out var attributeData))
if (!HasBaseApiAttribute(symbol, out var attributeData))
{
return null;
}
Expand Down Expand Up @@ -422,11 +422,11 @@ bool HasWrongSetOfParameters(IMethodSymbol method)
return false;
}

// We assume that the class can be decorated with an optional WebServiceAttribute
bool HasWebServiceAttribute(ITypeSymbol classSymbol, out AttributeData? webServiceAttribute)
// We assume that the class can be decorated with an optional BaseApiFromAttribute
bool HasBaseApiAttribute(ITypeSymbol classSymbol, out AttributeData? baseApiAttribute)
{
webServiceAttribute = classSymbol.GetAttribute(_webserviceAttributeSymbol);
return webServiceAttribute is not null;
baseApiAttribute = classSymbol.GetAttribute(_baseApiAttributeSymbol);
return baseApiAttribute is not null;
}

// We assume that the request DTO it is decorated with one RouteAttribute
Expand Down
1 change: 1 addition & 0 deletions src/WebsiteHost/Api/AuthN/AuthenticationApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

namespace WebsiteHost.Api.AuthN;

[BaseApiFrom("/api")]
public class AuthenticationApi : IWebApiService
{
private readonly IAuthenticationApplication _authenticationApplication;
Expand Down
1 change: 1 addition & 0 deletions src/WebsiteHost/Api/FeatureFlags/FeatureFlagsApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

namespace WebsiteHost.Api.FeatureFlags;

[BaseApiFrom("/api")]
public class FeatureFlagsApi : IWebApiService
{
private readonly ICallerContextFactory _callerFactory;
Expand Down
1 change: 1 addition & 0 deletions src/WebsiteHost/Api/Health/HealthApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace WebsiteHost.Api.Health;

[BaseApiFrom("/api")]
public sealed class HealthApi : IWebApiService
{
public async Task<ApiResult<string, HealthCheckResponse>> Check(HealthCheckRequest request,
Expand Down
1 change: 1 addition & 0 deletions src/WebsiteHost/Api/Recording/RecordingApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

namespace WebsiteHost.Api.Recording;

[BaseApiFrom("/api")]
public sealed class RecordingApi : IWebApiService
{
private readonly ICallerContextFactory _callerFactory;
Expand Down

0 comments on commit 90b19b8

Please sign in to comment.