@ -0,0 +1,62 @@ | |||
internal static class Extensions | |||
{ | |||
public static IServiceCollection AddReverseProxy(this IServiceCollection services, IConfiguration configuration) | |||
{ | |||
services.AddReverseProxy().LoadFromConfig(configuration.GetRequiredSection("ReverseProxy")); | |||
return services; | |||
} | |||
public static IServiceCollection AddHealthChecks(this IServiceCollection services, IConfiguration configuration) | |||
{ | |||
services.AddHealthChecks() | |||
.AddUrlGroup(_ => new Uri(configuration.GetRequiredValue("CatalogUrlHC")), name: "catalogapi-check", tags: new string[] { "catalogapi" }) | |||
.AddUrlGroup(_ => new Uri(configuration.GetRequiredValue("OrderingUrlHC")), name: "orderingapi-check", tags: new string[] { "orderingapi" }) | |||
.AddUrlGroup(_ => new Uri(configuration.GetRequiredValue("BasketUrlHC")), name: "basketapi-check", tags: new string[] { "basketapi" }) | |||
.AddUrlGroup(_ => new Uri(configuration.GetRequiredValue("IdentityUrlHC")), name: "identityapi-check", tags: new string[] { "identityapi" }); | |||
return services; | |||
} | |||
public static IServiceCollection AddApplicationServices(this IServiceCollection services) | |||
{ | |||
// Register delegating handlers | |||
services.AddTransient<HttpClientAuthorizationDelegatingHandler>(); | |||
// Register http services | |||
services.AddHttpClient<IOrderApiClient, OrderApiClient>() | |||
.AddHttpMessageHandler<HttpClientAuthorizationDelegatingHandler>(); | |||
return services; | |||
} | |||
public static IServiceCollection AddGrpcServices(this IServiceCollection services) | |||
{ | |||
services.AddTransient<GrpcExceptionInterceptor>(); | |||
services.AddScoped<IBasketService, BasketService>(); | |||
services.AddGrpcClient<Basket.BasketClient>((services, options) => | |||
{ | |||
var basketApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcBasket; | |||
options.Address = new Uri(basketApi); | |||
}).AddInterceptor<GrpcExceptionInterceptor>(); | |||
services.AddScoped<ICatalogService, CatalogService>(); | |||
services.AddGrpcClient<Catalog.CatalogClient>((services, options) => | |||
{ | |||
var catalogApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcCatalog; | |||
options.Address = new Uri(catalogApi); | |||
}).AddInterceptor<GrpcExceptionInterceptor>(); | |||
services.AddScoped<IOrderingService, OrderingService>(); | |||
services.AddGrpcClient<GrpcOrdering.OrderingGrpc.OrderingGrpcClient>((services, options) => | |||
{ | |||
var orderingApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcOrdering; | |||
options.Address = new Uri(orderingApi); | |||
}).AddInterceptor<GrpcExceptionInterceptor>(); | |||
return services; | |||
} | |||
} |
@ -1,33 +0,0 @@ | |||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Filters | |||
{ | |||
namespace Basket.API.Infrastructure.Filters | |||
{ | |||
public class AuthorizeCheckOperationFilter : IOperationFilter | |||
{ | |||
public void Apply(OpenApiOperation operation, OperationFilterContext context) | |||
{ | |||
// Check for authorize attribute | |||
var hasAuthorize = context.MethodInfo.DeclaringType.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any() || | |||
context.MethodInfo.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any(); | |||
if (!hasAuthorize) return; | |||
operation.Responses.TryAdd("401", new OpenApiResponse { Description = "Unauthorized" }); | |||
operation.Responses.TryAdd("403", new OpenApiResponse { Description = "Forbidden" }); | |||
var oAuthScheme = new OpenApiSecurityScheme | |||
{ | |||
Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "oauth2" } | |||
}; | |||
operation.Security = new List<OpenApiSecurityRequirement> | |||
{ | |||
new() | |||
{ | |||
[ oAuthScheme ] = new [] { "Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator" } | |||
} | |||
}; | |||
} | |||
} | |||
} | |||
} |
@ -1,145 +1,38 @@ | |||
var builder = WebApplication.CreateBuilder(args); | |||
builder.Services.AddHealthChecks() | |||
.AddCheck("self", () => HealthCheckResult.Healthy()) | |||
.AddUrlGroup(new Uri(builder.Configuration["CatalogUrlHC"]), name: "catalogapi-check", tags: new string[] { "catalogapi" }) | |||
.AddUrlGroup(new Uri(builder.Configuration["OrderingUrlHC"]), name: "orderingapi-check", tags: new string[] { "orderingapi" }) | |||
.AddUrlGroup(new Uri(builder.Configuration["BasketUrlHC"]), name: "basketapi-check", tags: new string[] { "basketapi" }) | |||
.AddUrlGroup(new Uri(builder.Configuration["IdentityUrlHC"]), name: "identityapi-check", tags: new string[] { "identityapi" }) | |||
.AddUrlGroup(new Uri(builder.Configuration["PaymentUrlHC"]), name: "paymentapi-check", tags: new string[] { "paymentapi" }); | |||
builder.AddServiceDefaults(); | |||
builder.Services.Configure<UrlsConfig>(builder.Configuration.GetSection("urls")); | |||
builder.Services.AddControllers() | |||
.AddJsonOptions(options => options.JsonSerializerOptions.WriteIndented = true); | |||
builder.Services.AddSwaggerGen(options => | |||
{ | |||
options.SwaggerDoc("v1", new OpenApiInfo | |||
{ | |||
Title = "Shopping Aggregator for Mobile Clients", | |||
Version = "v1", | |||
Description = "Shopping Aggregator for Mobile Clients" | |||
}); | |||
var identityUrl = builder.Configuration.GetSection("Identity").GetValue<string>("ExternalUrl"); | |||
options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme | |||
{ | |||
Type = SecuritySchemeType.OAuth2, | |||
Flows = new OpenApiOAuthFlows() | |||
{ | |||
Implicit = new OpenApiOAuthFlow() | |||
{ | |||
AuthorizationUrl = new Uri($"{identityUrl}/connect/authorize"), | |||
TokenUrl = new Uri($"{identityUrl}/connect/token"), | |||
Scopes = new Dictionary<string, string>() | |||
{ | |||
{ "mobileshoppingagg", "Shopping Aggregator for Mobile Clients" } | |||
} | |||
} | |||
} | |||
}); | |||
options.OperationFilter<AuthorizeCheckOperationFilter>(); | |||
}); | |||
builder.Services.AddReverseProxy(builder.Configuration); | |||
builder.Services.AddControllers(); | |||
builder.Services.AddHealthChecks(builder.Configuration); | |||
builder.Services.AddCors(options => | |||
{ | |||
// TODO: Read allowed origins from configuration | |||
options.AddPolicy("CorsPolicy", | |||
builder => builder | |||
.SetIsOriginAllowed((host) => true) | |||
.AllowAnyMethod() | |||
.AllowAnyHeader() | |||
.SetIsOriginAllowed((host) => true) | |||
.AllowCredentials()); | |||
}); | |||
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("sub"); | |||
builder.Services.AddApplicationServices(); | |||
builder.Services.AddGrpcServices(); | |||
var identityUrl = builder.Configuration.GetValue<string>("urls:identity"); | |||
builder.Services.AddAuthentication(options => | |||
{ | |||
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; | |||
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; | |||
}) | |||
.AddJwtBearer(options => | |||
{ | |||
options.Authority = identityUrl; | |||
options.RequireHttpsMetadata = false; | |||
options.Audience = "mobileshoppingagg"; | |||
options.TokenValidationParameters = new TokenValidationParameters | |||
{ | |||
ValidateAudience = false | |||
}; | |||
}); | |||
builder.Services.AddAuthorization(options => | |||
{ | |||
options.AddPolicy("ApiScope", policy => | |||
{ | |||
policy.RequireAuthenticatedUser(); | |||
policy.RequireClaim("scope", "mobileshoppingagg"); | |||
}); | |||
}); | |||
builder.Services.AddTransient<HttpClientAuthorizationDelegatingHandler>(); | |||
builder.Services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); | |||
builder.Services.AddHttpClient<IOrderApiClient, OrderApiClient>(); | |||
builder.Services.AddTransient<GrpcExceptionInterceptor>(); | |||
builder.Services.AddScoped<IBasketService, BasketService>(); | |||
builder.Services.AddGrpcClient<Basket.BasketClient>((services, options) => | |||
{ | |||
var basketApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcBasket; | |||
options.Address = new Uri(basketApi); | |||
}).AddInterceptor<GrpcExceptionInterceptor>(); | |||
builder.Services.AddScoped<ICatalogService, CatalogService>(); | |||
builder.Services.AddGrpcClient<Catalog.CatalogClient>((services, options) => | |||
{ | |||
var catalogApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcCatalog; | |||
options.Address = new Uri(catalogApi); | |||
}).AddInterceptor<GrpcExceptionInterceptor>(); | |||
builder.Services.AddScoped<IOrderingService, OrderingService>(); | |||
builder.Services.AddGrpcClient<GrpcOrdering.OrderingGrpc.OrderingGrpcClient>((services, options) => | |||
{ | |||
var orderingApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcOrdering; | |||
options.Address = new Uri(orderingApi); | |||
}).AddInterceptor<GrpcExceptionInterceptor>(); | |||
builder.Services.Configure<UrlsConfig>(builder.Configuration.GetSection("urls")); | |||
var app = builder.Build(); | |||
var pathBase = app.Configuration["PATH_BASE"]; | |||
if (!string.IsNullOrEmpty(pathBase)) | |||
{ | |||
app.UsePathBase(pathBase); | |||
} | |||
app.UseSwagger().UseSwaggerUI(c => | |||
{ | |||
c.SwaggerEndpoint($"{(!string.IsNullOrEmpty(pathBase) ? pathBase : string.Empty)}/swagger/v1/swagger.json", "Purchase BFF V1"); | |||
app.UseServiceDefaults(); | |||
c.OAuthClientId("mobileshoppingaggswaggerui"); | |||
c.OAuthClientSecret(string.Empty); | |||
c.OAuthRealm(string.Empty); | |||
c.OAuthAppName("Purchase BFF Swagger UI"); | |||
}); | |||
app.UseHttpsRedirection(); | |||
app.UseRouting(); | |||
app.UseCors("CorsPolicy"); | |||
app.UseAuthentication(); | |||
app.UseAuthorization(); | |||
app.MapDefaultControllerRoute(); | |||
app.MapControllers(); | |||
app.MapHealthChecks("/hc", new HealthCheckOptions() | |||
{ | |||
Predicate = _ => true, | |||
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse | |||
}); | |||
app.MapHealthChecks("/liveness", new HealthCheckOptions | |||
{ | |||
Predicate = r => r.Name.Contains("self") | |||
}); | |||
app.MapReverseProxy(); | |||
await app.RunAsync(); |
@ -1,20 +1,138 @@ | |||
{ | |||
"Logging": { | |||
"LogLevel": { | |||
"Default": "Information", | |||
"Microsoft.AspNetCore": "Warning", | |||
"System.Net.Http": "Warning" | |||
} | |||
}, | |||
"OpenApi": { | |||
"Endpoint": { | |||
"Name": "Purchase BFF V1" | |||
}, | |||
"Document": { | |||
"Description": "Shopping Aggregator for Mobile Clients", | |||
"Title": "Shopping Aggregator for Mobile Clients", | |||
"Version": "v1" | |||
}, | |||
"Auth": { | |||
"ClientId": "mobileshoppingaggswaggerui", | |||
"AppName": "Mobile shopping BFF Swagger UI" | |||
} | |||
}, | |||
"Identity": { | |||
"Url": "http://localhost:5105", | |||
"Audience": "mobileshoppingagg" | |||
"Url": "http://localhost:5223", | |||
"Audience": "mobileshoppingagg", | |||
"Scopes": { | |||
"webshoppingagg": "Shopping Aggregator for Mobile Clients" | |||
} | |||
}, | |||
"Logging": { | |||
"Debug": { | |||
"IncludeScopes": false, | |||
"LogLevel": { | |||
"Default": "Warning" | |||
"ReverseProxy": { | |||
"Routes": { | |||
"c-short": { | |||
"ClusterId": "catalog", | |||
"Match": { | |||
"Path": "c/{**catch-all}" | |||
}, | |||
"Transforms": [ | |||
{ "PathRemovePrefix": "/c" } | |||
] | |||
}, | |||
"c-long": { | |||
"ClusterId": "catalog", | |||
"Match": { | |||
"Path": "catalog-api/{**catch-all}" | |||
}, | |||
"Transforms": [ | |||
{ "PathRemovePrefix": "/catalog-api" } | |||
] | |||
}, | |||
"b-short": { | |||
"ClusterId": "basket", | |||
"Match": { | |||
"Path": "b/{**catch-all}" | |||
}, | |||
"Transforms": [ | |||
{ "PathRemovePrefix": "/b" } | |||
] | |||
}, | |||
"b-long": { | |||
"ClusterId": "basket", | |||
"Match": { | |||
"Path": "basket-api/{**catch-all}" | |||
}, | |||
"Transforms": [ | |||
{ "PathRemovePrefix": "/basket-api" } | |||
] | |||
}, | |||
"o-short": { | |||
"ClusterId": "orders", | |||
"Match": { | |||
"Path": "o/{**catch-all}" | |||
}, | |||
"Transforms": [ | |||
{ "PathRemovePrefix": "/o" } | |||
] | |||
}, | |||
"o-long": { | |||
"ClusterId": "orders", | |||
"Match": { | |||
"Path": "ordering-api/{**catch-all}" | |||
}, | |||
"Transforms": [ | |||
{ "PathRemovePrefix": "/ordering-api" } | |||
] | |||
}, | |||
"h-long": { | |||
"ClusterId": "signalr", | |||
"Match": { | |||
"Path": "hub/notificationhub/{**catch-all}" | |||
} | |||
} | |||
}, | |||
"Console": { | |||
"IncludeScopes": false, | |||
"LogLevel": { | |||
"Default": "Warning" | |||
"Clusters": { | |||
"basket": { | |||
"Destinations": { | |||
"destination0": { | |||
"Address": "http://localhost:5221" | |||
} | |||
} | |||
}, | |||
"catalog": { | |||
"Destinations": { | |||
"destination0": { | |||
"Address": "http://localhost:5222" | |||
} | |||
} | |||
}, | |||
"orders": { | |||
"Destinations": { | |||
"destination0": { | |||
"Address": "http://localhost:5224" | |||
} | |||
} | |||
}, | |||
"signalr": { | |||
"Destinations": { | |||
"destination0": { | |||
"Address": "http://localhost:5225" | |||
} | |||
} | |||
} | |||
} | |||
} | |||
}, | |||
"Urls": { | |||
"Basket": "http://localhost:5221", | |||
"Catalog": "http://localhost:5222", | |||
"Orders": "http://localhost:5224", | |||
"Identity": "http://localhost:5223", | |||
"Signalr": "http://localhost:5225", | |||
"GrpcBasket": "http://localhost:6221", | |||
"GrpcCatalog": "http://localhost:6222", | |||
"GrpcOrdering": "http://localhost:6224" | |||
}, | |||
"CatalogUrlHC": "http://localhost:5222/hc", | |||
"OrderingUrlHC": "http://localhost:5224/hc", | |||
"BasketUrlHC": "http://localhost:5221/hc", | |||
"IdentityUrlHC": "http://localhost:5223/hc" | |||
} |