@ -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); | 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 => | builder.Services.AddCors(options => | ||||
{ | { | ||||
// TODO: Read allowed origins from configuration | |||||
options.AddPolicy("CorsPolicy", | options.AddPolicy("CorsPolicy", | ||||
builder => builder | builder => builder | ||||
.SetIsOriginAllowed((host) => true) | |||||
.AllowAnyMethod() | .AllowAnyMethod() | ||||
.AllowAnyHeader() | .AllowAnyHeader() | ||||
.SetIsOriginAllowed((host) => true) | |||||
.AllowCredentials()); | .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 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.UseCors("CorsPolicy"); | ||||
app.UseAuthentication(); | app.UseAuthentication(); | ||||
app.UseAuthorization(); | app.UseAuthorization(); | ||||
app.MapDefaultControllerRoute(); | |||||
app.MapControllers(); | 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(); | 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": { | "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" | |||||
} | } |