Skip to content

Commit b60875c

Browse files
committed
Fix back-office runtime consolidation bugs
1 parent 958ddc0 commit b60875c

17 files changed

Lines changed: 201 additions & 31 deletions

application/AppGateway/appsettings.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,10 @@
155155
},
156156
"Metadata": {
157157
"HostnameKey": "App"
158-
}
158+
},
159+
"Transforms": [
160+
{ "RequestHeaderOriginalHost": true }
161+
]
159162
},
160163
"account-federation": {
161164
"ClusterId": "account-static",

application/account/Api/Endpoints/AuthenticationEndpoints.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ public sealed class AuthenticationEndpoints : IEndpoints
1313

1414
public void MapEndpoints(IEndpointRouteBuilder routes)
1515
{
16-
var group = routes.MapGroup(RoutesPrefix).WithTags("Authentication").WithGroupName(OpenApiDocumentNames.Account).RequireAuthorization().ProducesValidationProblem();
16+
var appHost = routes.ServiceProvider.GetRequiredService<IConfiguration>()["Hostnames:App"]!;
17+
18+
var group = routes.MapGroup(RoutesPrefix).WithTags("Authentication").WithGroupName(OpenApiDocumentNames.Account).RequireHost(appHost).RequireAuthorization().ProducesValidationProblem();
1719

1820
group.MapPost("/logout", async Task<ApiResult> (IMediator mediator)
1921
=> await mediator.Send(new LogoutCommand())
@@ -31,7 +33,9 @@ public void MapEndpoints(IEndpointRouteBuilder routes)
3133
=> await mediator.Send(new RevokeSessionCommand { Id = id })
3234
);
3335

34-
// Note: This endpoint must be called with the refresh token as Bearer token in the Authorization header
36+
// Note: This endpoint must be called with the refresh token as Bearer token in the Authorization header.
37+
// Internal-only endpoint (BlockInternalApiTransform rejects external callers); skips RequireHost so
38+
// backend-to-backend callers using the cluster's localhost address still reach it.
3539
routes.MapPost("/internal-api/account/authentication/refresh-authentication-tokens", async Task<ApiResult> (IMediator mediator)
3640
=> await mediator.Send(new RefreshAuthenticationTokensCommand())
3741
).WithGroupName(OpenApiDocumentNames.Account).DisableAntiforgery();

application/account/Api/Endpoints/BillingEndpoints.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ public sealed class BillingEndpoints : IEndpoints
1212

1313
public void MapEndpoints(IEndpointRouteBuilder routes)
1414
{
15-
var group = routes.MapGroup(RoutesPrefix).WithTags("Billing").WithGroupName(OpenApiDocumentNames.Account).RequireAuthorization().ProducesValidationProblem();
15+
var appHost = routes.ServiceProvider.GetRequiredService<IConfiguration>()["Hostnames:App"]!;
16+
17+
var group = routes.MapGroup(RoutesPrefix).WithTags("Billing").WithGroupName(OpenApiDocumentNames.Account).RequireHost(appHost).RequireAuthorization().ProducesValidationProblem();
1618

1719
group.MapGet("/payment-history", async Task<ApiResult<PaymentHistoryResponse>> ([AsParameters] GetPaymentHistoryQuery query, IMediator mediator)
1820
=> await mediator.Send(query)

application/account/Api/Endpoints/EmailAuthenticationEndpoints.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ public sealed class EmailAuthenticationEndpoints : IEndpoints
1212

1313
public void MapEndpoints(IEndpointRouteBuilder routes)
1414
{
15-
var group = routes.MapGroup(RoutesPrefix).WithTags("EmailAuthentication").WithGroupName(OpenApiDocumentNames.Account).RequireAuthorization().ProducesValidationProblem();
15+
var appHost = routes.ServiceProvider.GetRequiredService<IConfiguration>()["Hostnames:App"]!;
16+
17+
var group = routes.MapGroup(RoutesPrefix).WithTags("EmailAuthentication").WithGroupName(OpenApiDocumentNames.Account).RequireHost(appHost).RequireAuthorization().ProducesValidationProblem();
1618

1719
group.MapPost("/login/start", async Task<ApiResult<StartEmailLoginResponse>> (StartEmailLoginCommand command, IMediator mediator)
1820
=> await mediator.Send(command)

application/account/Api/Endpoints/ExternalAuthenticationEndpoints.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ public sealed class ExternalAuthenticationEndpoints : IEndpoints
1313

1414
public void MapEndpoints(IEndpointRouteBuilder routes)
1515
{
16-
var group = routes.MapGroup(RoutesPrefix).WithTags("ExternalAuthentication").WithGroupName(OpenApiDocumentNames.Account).RequireAuthorization().ProducesValidationProblem();
16+
var appHost = routes.ServiceProvider.GetRequiredService<IConfiguration>()["Hostnames:App"]!;
17+
18+
var group = routes.MapGroup(RoutesPrefix).WithTags("ExternalAuthentication").WithGroupName(OpenApiDocumentNames.Account).RequireHost(appHost).RequireAuthorization().ProducesValidationProblem();
1719

1820
group.MapGet("/{provider}/login/start", async Task<ApiResult<string>> (ExternalProviderType provider, [AsParameters] StartExternalLoginCommand command, IMediator mediator)
1921
=> await mediator.Send(command with { ProviderType = provider })

application/account/Api/Endpoints/StripeWebhookEndpoints.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ public sealed class StripeWebhookEndpoints : IEndpoints
1313

1414
public void MapEndpoints(IEndpointRouteBuilder routes)
1515
{
16-
var group = routes.MapGroup(RoutesPrefix).WithTags("StripeWebhook").WithGroupName(OpenApiDocumentNames.Account).RequireAuthorization().ProducesValidationProblem();
16+
var appHost = routes.ServiceProvider.GetRequiredService<IConfiguration>()["Hostnames:App"]!;
17+
18+
var group = routes.MapGroup(RoutesPrefix).WithTags("StripeWebhook").WithGroupName(OpenApiDocumentNames.Account).RequireHost(appHost).RequireAuthorization().ProducesValidationProblem();
1719

1820
// Two-phase webhook processing with pessimistic locking requires inline logic beyond 3-line convention
1921
group.MapPost("/", async Task<ApiResult> (HttpRequest request, IMediator mediator, ProcessPendingStripeEvents processPendingStripeEvents) =>

application/account/Api/Endpoints/SubscriptionEndpoints.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ public sealed class SubscriptionEndpoints : IEndpoints
1212

1313
public void MapEndpoints(IEndpointRouteBuilder routes)
1414
{
15-
var group = routes.MapGroup(RoutesPrefix).WithTags("Subscriptions").WithGroupName(OpenApiDocumentNames.Account).RequireAuthorization().ProducesValidationProblem();
15+
var appHost = routes.ServiceProvider.GetRequiredService<IConfiguration>()["Hostnames:App"]!;
16+
17+
var group = routes.MapGroup(RoutesPrefix).WithTags("Subscriptions").WithGroupName(OpenApiDocumentNames.Account).RequireHost(appHost).RequireAuthorization().ProducesValidationProblem();
1618

1719
group.MapGet("/pricing-catalog", async Task<ApiResult<PricingCatalogResponse>> ([AsParameters] GetPricingCatalogQuery query, IMediator mediator)
1820
=> await mediator.Send(query)

application/account/Api/Endpoints/TenantEndpoints.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ public sealed class TenantEndpoints : IEndpoints
1313

1414
public void MapEndpoints(IEndpointRouteBuilder routes)
1515
{
16-
var group = routes.MapGroup(RoutesPrefix).WithTags("Tenants").WithGroupName(OpenApiDocumentNames.Account).RequireAuthorization().ProducesValidationProblem();
16+
var appHost = routes.ServiceProvider.GetRequiredService<IConfiguration>()["Hostnames:App"]!;
17+
18+
var group = routes.MapGroup(RoutesPrefix).WithTags("Tenants").WithGroupName(OpenApiDocumentNames.Account).RequireHost(appHost).RequireAuthorization().ProducesValidationProblem();
1719

1820
group.MapGet("/current", async Task<ApiResult<TenantResponse>> (IMediator mediator)
1921
=> await mediator.Send(new GetCurrentTenantQuery())
@@ -35,6 +37,8 @@ public void MapEndpoints(IEndpointRouteBuilder routes)
3537
=> await mediator.Send(new RemoveTenantLogoCommand())
3638
);
3739

40+
// Internal-only endpoint (BlockInternalApiTransform rejects external callers); skips RequireHost so
41+
// backend-to-backend callers using the cluster's localhost address still reach it.
3842
routes.MapDelete("/internal-api/account/tenants/{id}", async Task<ApiResult> (TenantId id, IMediator mediator)
3943
=> await mediator.Send(new DeleteTenantCommand(id))
4044
).WithGroupName(OpenApiDocumentNames.Account);

application/account/Api/Endpoints/UserEndpoints.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ public sealed class UserEndpoints : IEndpoints
1313

1414
public void MapEndpoints(IEndpointRouteBuilder routes)
1515
{
16-
var group = routes.MapGroup(RoutesPrefix).WithTags("Users").WithGroupName(OpenApiDocumentNames.Account).RequireAuthorization().ProducesValidationProblem();
16+
var appHost = routes.ServiceProvider.GetRequiredService<IConfiguration>()["Hostnames:App"]!;
17+
18+
var group = routes.MapGroup(RoutesPrefix).WithTags("Users").WithGroupName(OpenApiDocumentNames.Account).RequireHost(appHost).RequireAuthorization().ProducesValidationProblem();
1719

1820
group.MapGet("/", async Task<ApiResult<UsersResponse>> ([AsParameters] GetUsersQuery query, IMediator mediator)
1921
=> await mediator.Send(query)

application/account/Api/Program.cs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,24 +29,44 @@
2929
var appHostname = app.Configuration["Hostnames:App"] ?? "app.unconfigured.invalid";
3030
var backOfficeHostname = app.Services.GetRequiredService<IOptions<BackOfficeHostOptions>>().Value.Host;
3131

32+
// Per-host bundle URLs. The process-wide PUBLIC_URL/CDN_URL env vars are set by AppHost for the
33+
// user-facing host only, so the back-office host must inject its own to avoid embedding the account
34+
// SPA's bundle URLs into back-office HTML.
35+
var appPublicUrl = Environment.GetEnvironmentVariable(SinglePageAppConfiguration.PublicUrlKey);
36+
var appCdnUrl = Environment.GetEnvironmentVariable(SinglePageAppConfiguration.CdnUrlKey);
37+
var backOfficePublicUrl = appPublicUrl is null ? null : ReplaceHost(appPublicUrl, appHostname, backOfficeHostname);
38+
var backOfficeCdnUrl = backOfficePublicUrl;
39+
3240
app
3341
.UseApiServices() // Add common configuration for all APIs like Swagger, HSTS, and DeveloperExceptionPage.
3442
.UseHostScopedSinglePageAppFallback(
3543
new HostScopedSinglePageApp(
3644
appHostname,
3745
"WebApp",
38-
context => context.RequestServices.GetRequiredService<IExecutionContext>().UserInfo
46+
context => context.RequestServices.GetRequiredService<IExecutionContext>().UserInfo,
47+
appPublicUrl,
48+
appCdnUrl
3949
),
4050
new HostScopedSinglePageApp(
4151
backOfficeHostname,
4252
"BackOfficeWebApp",
43-
BuildBackOfficeUserInfo
53+
BuildBackOfficeUserInfo,
54+
backOfficePublicUrl,
55+
backOfficeCdnUrl,
56+
BackOfficeIdentityDefaults.PolicyName
4457
)
4558
);
4659

4760
await app.RunAsync();
4861
return;
4962

63+
static string ReplaceHost(string url, string oldHost, string newHost)
64+
{
65+
var uri = new Uri(url);
66+
var builder = new UriBuilder(uri) { Host = uri.Host.Equals(oldHost, StringComparison.OrdinalIgnoreCase) ? newHost : uri.Host };
67+
return builder.Uri.ToString().TrimEnd('/');
68+
}
69+
5070
static UserInfo BuildBackOfficeUserInfo(HttpContext context)
5171
{
5272
var principal = context.User;

0 commit comments

Comments
 (0)