Enhancements/netcore2.1/asp.netcore2.1/gdprpull/737/head
@ -1,317 +0,0 @@ | |||
version: '3.4' | |||
# The default docker-compose.override file can use the "localhost" as the external name for testing web apps within the same dev machine. | |||
# The ESHOP_EXTERNAL_DNS_NAME_OR_IP environment variable is taken, by default, from the ".env" file defined like: | |||
# ESHOP_EXTERNAL_DNS_NAME_OR_IP=localhost | |||
# but values present in the environment vars at runtime will always override those defined inside the .env file | |||
# An external IP or DNS name has to be used (instead localhost and the 10.0.75.1 IP) when testing the Web apps and the Xamarin apps from remote machines/devices using the same WiFi, for instance. | |||
services: | |||
sql.data: | |||
environment: | |||
- SA_PASSWORD=Pass@word | |||
- ACCEPT_EULA=Y | |||
ports: | |||
- "5433:1433" # Important: In a production environment your should remove the external port | |||
nosql.data: | |||
ports: | |||
- "27017:27017" # Important: In a production environment your should remove the external port | |||
basket.data: | |||
ports: | |||
- "6379:6379" # Important: In a production environment your should remove the external port | |||
rabbitmq: | |||
ports: | |||
- "15672:15672" # Important: In a production environment your should remove the external port | |||
- "5672:5672" # Important: In a production environment your should remove the external port | |||
identity.api: | |||
environment: | |||
- ASPNETCORE_ENVIRONMENT=Development | |||
- ASPNETCORE_URLS=http://0.0.0.0:80 | |||
- SpaClient=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5104 | |||
- XamarinCallback=http://${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}:5105/xamarincallback #localhost do not work for UWP login, so we have to use "external" IP always | |||
- ConnectionString=${ESHOP_AZURE_IDENTITY_DB:-Server=sql.data;Database=Microsoft.eShopOnContainers.Service.IdentityDb;User Id=sa;Password=Pass@word} | |||
- MvcClient=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5100 #Local: You need to open your local dev-machine firewall at range 5100-5110. | |||
- LocationApiClient=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5109 | |||
- MarketingApiClient=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5110 | |||
- BasketApiClient=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5103 | |||
- OrderingApiClient=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5102 | |||
- MobileShoppingAggClient=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5120 | |||
- WebShoppingAggClient=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5121 | |||
- UseCustomizationData=True | |||
- ApplicationInsights__InstrumentationKey=${INSTRUMENTATION_KEY} | |||
- OrchestratorType=${ORCHESTRATOR_TYPE} | |||
ports: | |||
- "5105:80" | |||
basket.api: | |||
environment: | |||
- ASPNETCORE_ENVIRONMENT=Development | |||
- ASPNETCORE_URLS=http://0.0.0.0:80 | |||
- ConnectionString=${ESHOP_AZURE_REDIS_BASKET_DB:-basket.data} | |||
- identityUrl=http://identity.api #Local: You need to open your local dev-machine firewall at range 5100-5110. | |||
- IdentityUrlExternal=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5105 | |||
- EventBusConnection=${ESHOP_AZURE_SERVICE_BUS:-rabbitmq} | |||
- EventBusUserName=${ESHOP_SERVICE_BUS_USERNAME} | |||
- EventBusPassword=${ESHOP_SERVICE_BUS_PASSWORD} | |||
- AzureServiceBusEnabled=False | |||
- ApplicationInsights__InstrumentationKey=${INSTRUMENTATION_KEY} | |||
- OrchestratorType=${ORCHESTRATOR_TYPE} | |||
- UseLoadTest=${USE_LOADTEST:-False} | |||
ports: | |||
- "5103:80" # Important: In a production environment your should remove the external port (5103) kept here for microservice debugging purposes. | |||
# The API Gateway redirects and access through the internal port (80). | |||
catalog.api: | |||
environment: | |||
- ASPNETCORE_ENVIRONMENT=Development | |||
- ASPNETCORE_URLS=http://0.0.0.0:80 | |||
- ConnectionString=${ESHOP_AZURE_CATALOG_DB:-Server=sql.data;Database=Microsoft.eShopOnContainers.Services.CatalogDb;User Id=sa;Password=Pass@word} | |||
- PicBaseUrl=${ESHOP_AZURE_STORAGE_CATALOG_URL:-http://localhost:5202/api/v1/c/catalog/items/[0]/pic/} #Local: You need to open your local dev-machine firewall at range 5100-5110. | |||
- EventBusConnection=${ESHOP_AZURE_SERVICE_BUS:-rabbitmq} | |||
- EventBusUserName=${ESHOP_SERVICE_BUS_USERNAME} | |||
- EventBusPassword=${ESHOP_SERVICE_BUS_PASSWORD} | |||
- AzureStorageAccountName=${ESHOP_AZURE_STORAGE_CATALOG_NAME} | |||
- AzureStorageAccountKey=${ESHOP_AZURE_STORAGE_CATALOG_KEY} | |||
- UseCustomizationData=True | |||
- AzureServiceBusEnabled=False | |||
- AzureStorageEnabled=False | |||
- ApplicationInsights__InstrumentationKey=${INSTRUMENTATION_KEY} | |||
- OrchestratorType=${ORCHESTRATOR_TYPE} | |||
ports: | |||
- "5101:80" # Important: In a production environment your should remove the external port (5101) kept here for microservice debugging purposes. | |||
# The API Gateway redirects and access through the internal port (80). | |||
ordering.api: | |||
environment: | |||
- ASPNETCORE_ENVIRONMENT=Development | |||
- ASPNETCORE_URLS=http://0.0.0.0:80 | |||
- ConnectionString=${ESHOP_AZURE_ORDERING_DB:-Server=sql.data;Database=Microsoft.eShopOnContainers.Services.OrderingDb;User Id=sa;Password=Pass@word} | |||
- identityUrl=http://identity.api #Local: You need to open your local dev-machine firewall at range 5100-5110. | |||
- IdentityUrlExternal=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5105 | |||
- EventBusConnection=${ESHOP_AZURE_SERVICE_BUS:-rabbitmq} | |||
- EventBusUserName=${ESHOP_SERVICE_BUS_USERNAME} | |||
- EventBusPassword=${ESHOP_SERVICE_BUS_PASSWORD} | |||
- UseCustomizationData=True | |||
- AzureServiceBusEnabled=False | |||
- CheckUpdateTime=30000 | |||
- ApplicationInsights__InstrumentationKey=${INSTRUMENTATION_KEY} | |||
- OrchestratorType=${ORCHESTRATOR_TYPE} | |||
- UseLoadTest=${USE_LOADTEST:-False} | |||
ports: | |||
- "5102:80" # Important: In a production environment your should remove the external port (5102) kept here for microservice debugging purposes. | |||
# The API Gateway redirects and access through the internal port (80). | |||
ordering.backgroundtasks: | |||
environment: | |||
- ASPNETCORE_ENVIRONMENT=Development | |||
- ASPNETCORE_URLS=http://0.0.0.0:80 | |||
- ConnectionString=${ESHOP_AZURE_ORDERING_DB:-Server=sql.data;Database=Microsoft.eShopOnContainers.Services.OrderingDb;User Id=sa;Password=Pass@word} | |||
- EventBusConnection=${ESHOP_AZURE_SERVICE_BUS:-rabbitmq} | |||
- EventBusUserName=${ESHOP_SERVICE_BUS_USERNAME} | |||
- EventBusPassword=${ESHOP_SERVICE_BUS_PASSWORD} | |||
- UseCustomizationData=True | |||
- AzureServiceBusEnabled=False | |||
- CheckUpdateTime=30000 | |||
- GracePeriodTime=1 | |||
- ApplicationInsights__InstrumentationKey=${INSTRUMENTATION_KEY} | |||
- OrchestratorType=${ORCHESTRATOR_TYPE} | |||
- UseLoadTest=${USE_LOADTEST:-False} | |||
ports: | |||
- "5111:80" | |||
marketing.api: | |||
environment: | |||
- ASPNETCORE_ENVIRONMENT=Development | |||
- ASPNETCORE_URLS=http://0.0.0.0:80 | |||
- ConnectionString=${ESHOP_AZURE_MARKETING_DB:-Server=sql.data;Database=Microsoft.eShopOnContainers.Services.MarketingDb;User Id=sa;Password=Pass@word} | |||
- MongoConnectionString=${ESHOP_AZURE_COSMOSDB:-mongodb://nosql.data} | |||
- MongoDatabase=MarketingDb | |||
- EventBusConnection=${ESHOP_AZURE_SERVICE_BUS:-rabbitmq} | |||
- EventBusUserName=${ESHOP_SERVICE_BUS_USERNAME} | |||
- EventBusPassword=${ESHOP_SERVICE_BUS_PASSWORD} | |||
- identityUrl=http://identity.api #Local: You need to open your local dev-machine firewall at range 5100-5110. | |||
- IdentityUrlExternal=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5105 | |||
- CampaignDetailFunctionUri=${ESHOP_AZUREFUNC_CAMPAIGN_DETAILS_URI} | |||
- PicBaseUrl=${ESHOP_AZURE_STORAGE_MARKETING_URL:-http://localhost:5110/api/v1/campaigns/[0]/pic/} | |||
- AzureStorageAccountName=${ESHOP_AZURE_STORAGE_MARKETING_NAME} | |||
- AzureStorageAccountKey=${ESHOP_AZURE_STORAGE_MARKETING_KEY} | |||
- AzureServiceBusEnabled=False | |||
- AzureStorageEnabled=False | |||
- ApplicationInsights__InstrumentationKey=${INSTRUMENTATION_KEY} | |||
- OrchestratorType=${ORCHESTRATOR_TYPE} | |||
- UseLoadTest=${USE_LOADTEST:-False} | |||
ports: | |||
- "5110:80" # Important: In a production environment your should remove the external port (5110) kept here for microservice debugging purposes. | |||
# The API Gateway redirects and access through the internal port (80). | |||
payment.api: | |||
environment: | |||
- ASPNETCORE_ENVIRONMENT=Development | |||
- ASPNETCORE_URLS=http://0.0.0.0:80 | |||
- EventBusConnection=${ESHOP_AZURE_SERVICE_BUS:-rabbitmq} | |||
- EventBusUserName=${ESHOP_SERVICE_BUS_USERNAME} | |||
- EventBusPassword=${ESHOP_SERVICE_BUS_PASSWORD} | |||
- AzureServiceBusEnabled=False | |||
- ApplicationInsights__InstrumentationKey=${INSTRUMENTATION_KEY} | |||
- OrchestratorType=${ORCHESTRATOR_TYPE} | |||
ports: | |||
- "5108:80" # Important: In a production environment your should remove the external port (5108) kept here for microservice debugging purposes. | |||
# The API Gateway redirects and access through the internal port (80). | |||
locations.api: | |||
environment: | |||
- ASPNETCORE_ENVIRONMENT=Development | |||
- ASPNETCORE_URLS=http://0.0.0.0:80 | |||
- ConnectionString=${ESHOP_AZURE_COSMOSDB:-mongodb://nosql.data} | |||
- Database=LocationsDb | |||
- identityUrl=http://identity.api #Local: You need to open your local dev-machine firewall at range 5100-5110. | |||
- IdentityUrlExternal=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5105 | |||
- EventBusConnection=${ESHOP_AZURE_SERVICE_BUS:-rabbitmq} | |||
- EventBusUserName=${ESHOP_SERVICE_BUS_USERNAME} | |||
- EventBusPassword=${ESHOP_SERVICE_BUS_PASSWORD} | |||
- AzureServiceBusEnabled=False | |||
- ApplicationInsights__InstrumentationKey=${INSTRUMENTATION_KEY} | |||
- OrchestratorType=${ORCHESTRATOR_TYPE} | |||
- UseLoadTest=${USE_LOADTEST:-False} | |||
ports: | |||
- "5109:80" # Important: In a production environment your should remove the external port (5109) kept here for microservice debugging purposes. | |||
# The API Gateway redirects and access through the internal port (80). | |||
mobileshoppingapigw: | |||
environment: | |||
- ASPNETCORE_ENVIRONMENT=Development | |||
- IdentityUrl=http://identity.api #Local: You need to open your local dev-machine firewall at range 5100-5110. | |||
ports: | |||
- "5200:80" | |||
volumes: | |||
- ./src/ApiGateways/Mobile.Bff.Shopping/apigw:${ESHOP_OCELOT_VOLUME_SPEC:-/app/configuration} | |||
mobilemarketingapigw: | |||
environment: | |||
- ASPNETCORE_ENVIRONMENT=Development | |||
- IdentityUrl=http://identity.api #Local: You need to open your local dev-machine firewall at range 5100-5110. | |||
ports: | |||
- "5201:80" | |||
volumes: | |||
- ./src/ApiGateways/Mobile.Bff.Marketing/apigw:${ESHOP_OCELOT_VOLUME_SPEC:-/app/configuration} | |||
webshoppingapigw: | |||
environment: | |||
- ASPNETCORE_ENVIRONMENT=Development | |||
- IdentityUrl=http://identity.api #Local: You need to open your local dev-machine firewall at range 5100-5110. | |||
ports: | |||
- "5202:80" | |||
volumes: | |||
- ./src/ApiGateways/Web.Bff.Shopping/apigw:${ESHOP_OCELOT_VOLUME_SPEC:-/app/configuration} | |||
webmarketingapigw: | |||
environment: | |||
- ASPNETCORE_ENVIRONMENT=Development | |||
- IdentityUrl=http://identity.api #Local: You need to open your local dev-machine firewall at range 5100-5110. | |||
ports: | |||
- "5203:80" | |||
volumes: | |||
- ./src/ApiGateways/Web.Bff.Marketing/apigw:${ESHOP_OCELOT_VOLUME_SPEC:-/app/configuration} | |||
mobileshoppingagg: | |||
environment: | |||
- ASPNETCORE_ENVIRONMENT=Development | |||
- urls__basket=http://basket.api | |||
- urls__catalog=http://catalog.api | |||
- urls__orders=http://ordering.api | |||
- urls__identity=http://identity.api #Local: You need to open your local dev-machine firewall at range 5100-5110. | |||
ports: | |||
- "5120:80" # Important: In a production environment your should remove the external port (5120) kept here for microservice debugging purposes. | |||
# The API Gateway redirects and access through the internal port (80). | |||
webshoppingagg: | |||
environment: | |||
- ASPNETCORE_ENVIRONMENT=Development | |||
- urls__basket=http://basket.api | |||
- urls__catalog=http://catalog.api | |||
- urls__orders=http://ordering.api | |||
- urls__identity=http://identity.api #Local: You need to open your local dev-machine firewall at range 5100-5110. | |||
ports: | |||
- "5121:80" # Important: In a production environment your should remove the external port (5121) kept here for microservice debugging purposes. | |||
# The API Gateway redirects and access through the internal port (80). | |||
ordering.signalrhub: | |||
environment: | |||
- ASPNETCORE_ENVIRONMENT=Development | |||
- ASPNETCORE_URLS=http://0.0.0.0:80 | |||
- EventBusConnection=${ESHOP_AZURE_SERVICE_BUS:-rabbitmq} | |||
- EventBusUserName=${ESHOP_SERVICE_BUS_USERNAME} | |||
- EventBusPassword=${ESHOP_SERVICE_BUS_PASSWORD} | |||
- AzureServiceBusEnabled=False | |||
- ApplicationInsights__InstrumentationKey=${INSTRUMENTATION_KEY} | |||
- OrchestratorType=${ORCHESTRATOR_TYPE} | |||
- identityUrl=http://identity.api #Local: You need to open your local dev-machine firewall at range 5100-5110. | |||
ports: | |||
- "5112:80" | |||
webstatus: | |||
environment: | |||
- ASPNETCORE_ENVIRONMENT=Development | |||
- ASPNETCORE_URLS=http://0.0.0.0:80 | |||
- CatalogUrl=http://catalog.api/hc | |||
- OrderingUrl=http://ordering.api/hc | |||
- OrderingBackgroundTasksUrl=http://ordering.backgroundtasks/hc | |||
- BasketUrl=http://basket.api/hc | |||
- IdentityUrl=http://identity.api/hc | |||
- LocationsUrl=http://locations.api/hc | |||
- MarketingUrl=http://marketing.api/hc | |||
- PaymentUrl=http://payment.api/hc | |||
- mvc=http://webmvc/hc | |||
- spa=http://webspa/hc | |||
- ApplicationInsights__InstrumentationKey=${INSTRUMENTATION_KEY} | |||
- OrchestratorType=${ORCHESTRATOR_TYPE} | |||
ports: | |||
- "5107:80" | |||
webspa: | |||
environment: | |||
- ASPNETCORE_ENVIRONMENT=Development | |||
- ASPNETCORE_URLS=http://0.0.0.0:80 | |||
- IdentityUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5105 #Local: You need to open your local dev-machine firewall at range 5100-5105. at range 5100-5105. | |||
- PurchaseUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5202 | |||
- MarketingUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5203 | |||
- CatalogUrlHC=http://catalog.api/hc | |||
- OrderingUrlHC=http://ordering.api/hc | |||
- IdentityUrlHC=http://identity.api/hc #Local: Use ${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}, if using external IP or DNS name from browser. | |||
- BasketUrlHC=http://basket.api/hc | |||
- MarketingUrlHC=http://marketing.api/hc | |||
- PaymentUrlHC=http://payment.api/hc | |||
- UseCustomizationData=True | |||
- ApplicationInsights__InstrumentationKey=${INSTRUMENTATION_KEY} | |||
- OrchestratorType=${ORCHESTRATOR_TYPE} | |||
- SignalrHubUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5202 | |||
ports: | |||
- "5104:80" | |||
webmvc: | |||
environment: | |||
- ASPNETCORE_ENVIRONMENT=Development | |||
- ASPNETCORE_URLS=http://0.0.0.0:80 | |||
- PurchaseUrl=http://webshoppingapigw | |||
- IdentityUrl=http://10.0.75.1:5105 # Local Mac: Use http://docker.for.mac.localhost:5105 || Local Windows: Use 10.0.75.1 in a "Docker for Windows" environment, if using "localhost" from browser. || #Remote access: Use ${ESHOP_EXTERNAL_DNS_NAME_OR_IP} if using external IP or DNS name from browser. | |||
- MarketingUrl=http://webmarketingapigw | |||
- CatalogUrlHC=http://catalog.api/hc | |||
- OrderingUrlHC=http://ordering.api/hc | |||
- IdentityUrlHC=http://identity.api/hc #Local: Use ${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}, if using external IP or DNS name from browser. | |||
- BasketUrlHC=http://basket.api/hc | |||
- MarketingUrlHC=http://marketing.api/hc | |||
- PaymentUrlHC=http://payment.api/hc | |||
- SignalrHubUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5202 | |||
- UseCustomizationData=True | |||
- ApplicationInsights__InstrumentationKey=${INSTRUMENTATION_KEY} | |||
- OrchestratorType=${ORCHESTRATOR_TYPE} | |||
- UseLoadTest=${USE_LOADTEST:-False} | |||
ports: | |||
- "5100:80" | |||
@ -1,8 +0,0 @@ | |||
{ | |||
"urls": { | |||
"basket": "http://localhost:55105", | |||
"catalog": "http://localhost:55101", | |||
"orders": "http://localhost:55102", | |||
"identity": "http://localhost:55105" | |||
} | |||
} |
@ -1,8 +0,0 @@ | |||
{ | |||
"urls": { | |||
"basket": "http://localhost:55105", | |||
"catalog": "http://localhost:55101", | |||
"orders": "http://localhost:55102", | |||
"identity": "http://localhost:55105" | |||
} | |||
} |
@ -1,24 +0,0 @@ | |||
{ | |||
"Logging": { | |||
"IncludeScopes": false, | |||
"LogLevel": { | |||
"Default": "Debug", | |||
"System": "Information", | |||
"Microsoft": "Information" | |||
} | |||
}, | |||
"IdentityUrl": "http://localhost:5105", | |||
"ConnectionString": "127.0.0.1", | |||
"AzureServiceBusEnabled": false, | |||
"SubscriptionClientName": "Basket", | |||
"ApplicationInsights": { | |||
"InstrumentationKey": "" | |||
}, | |||
"EventBusRetryCount": 5, | |||
"UseVault": false, | |||
"Vault": { | |||
"Name": "eshop", | |||
"ClientId": "your-clien-id", | |||
"ClientSecret": "your-client-secret" | |||
} | |||
} |
@ -1,30 +0,0 @@ | |||
ARG NODE_IMAGE=node:8.11 | |||
FROM microsoft/dotnet:2.1-aspnetcore-runtime AS base | |||
WORKDIR /app | |||
EXPOSE 80 | |||
FROM microsoft/dotnet:2.1-sdk as dotnet-build | |||
WORKDIR /src | |||
FROM ${NODE_IMAGE} as node-build | |||
WORKDIR /web | |||
COPY src/Services/Identity/Identity.API . | |||
RUN npm install -g bower@1.8.4 | |||
RUN bower install --allow-root | |||
FROM dotnet-build as build | |||
WORKDIR /src/src/Services/Identity/Identity.API/wwwroot | |||
COPY --from=node-build /web/wwwroot . | |||
WORKDIR /src | |||
COPY . . | |||
WORKDIR /src/src/Services/Identity/Identity.API | |||
RUN dotnet restore -nowarn:msb3202,nu1503 | |||
RUN dotnet build --no-restore -c Release -o /app | |||
FROM build AS publish | |||
RUN dotnet publish --no-restore -c Release -o /app | |||
FROM base AS final | |||
WORKDIR /app | |||
COPY --from=publish /app . | |||
ENTRYPOINT ["dotnet", "Identity.API.dll"] |
@ -1,184 +0,0 @@ | |||
using Autofac; | |||
using Autofac.Extensions.DependencyInjection; | |||
using IdentityServer4.Services; | |||
using Microsoft.ApplicationInsights.Extensibility; | |||
using Microsoft.ApplicationInsights.ServiceFabric; | |||
using Microsoft.AspNetCore.Builder; | |||
using Microsoft.AspNetCore.DataProtection; | |||
using Microsoft.AspNetCore.Hosting; | |||
using Microsoft.AspNetCore.Identity; | |||
using Microsoft.EntityFrameworkCore; | |||
using Microsoft.eShopOnContainers.Services.Identity.API.Certificates; | |||
using Microsoft.eShopOnContainers.Services.Identity.API.Data; | |||
using Microsoft.eShopOnContainers.Services.Identity.API.Models; | |||
using Microsoft.eShopOnContainers.Services.Identity.API.Services; | |||
using Microsoft.Extensions.Configuration; | |||
using Microsoft.Extensions.DependencyInjection; | |||
using Microsoft.Extensions.HealthChecks; | |||
using Microsoft.Extensions.Logging; | |||
using StackExchange.Redis; | |||
using System; | |||
using System.Reflection; | |||
namespace Microsoft.eShopOnContainers.Services.Identity.API | |||
{ | |||
public class Startup | |||
{ | |||
public Startup(IConfiguration configuration) | |||
{ | |||
Configuration = configuration; | |||
} | |||
public IConfiguration Configuration { get; } | |||
// This method gets called by the runtime. Use this method to add services to the container. | |||
public IServiceProvider ConfigureServices(IServiceCollection services) | |||
{ | |||
RegisterAppInsights(services); | |||
// Add framework services. | |||
services.AddDbContext<ApplicationDbContext>(options => | |||
options.UseSqlServer(Configuration["ConnectionString"], | |||
sqlServerOptionsAction: sqlOptions => | |||
{ | |||
sqlOptions.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name); | |||
//Configuring Connection Resiliency: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency | |||
sqlOptions.EnableRetryOnFailure(maxRetryCount: 15, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null); | |||
})); | |||
services.AddIdentity<ApplicationUser, IdentityRole>() | |||
.AddEntityFrameworkStores<ApplicationDbContext>() | |||
.AddDefaultTokenProviders(); | |||
services.Configure<AppSettings>(Configuration); | |||
services.AddMvc(); | |||
if (Configuration.GetValue<string>("IsClusterEnv") == bool.TrueString) | |||
{ | |||
services.AddDataProtection(opts => | |||
{ | |||
opts.ApplicationDiscriminator = "eshop.identity"; | |||
}) | |||
.PersistKeysToRedis(ConnectionMultiplexer.Connect(Configuration["DPConnectionString"]), "DataProtection-Keys"); | |||
} | |||
services.AddHealthChecks(checks => | |||
{ | |||
var minutes = 1; | |||
if (int.TryParse(Configuration["HealthCheck:Timeout"], out var minutesParsed)) | |||
{ | |||
minutes = minutesParsed; | |||
} | |||
checks.AddSqlCheck("Identity_Db", Configuration["ConnectionString"], TimeSpan.FromMinutes(minutes)); | |||
}); | |||
services.AddTransient<ILoginService<ApplicationUser>, EFLoginService>(); | |||
services.AddTransient<IRedirectService, RedirectService>(); | |||
var connectionString = Configuration["ConnectionString"]; | |||
var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name; | |||
// Adds IdentityServer | |||
services.AddIdentityServer(x => x.IssuerUri = "null") | |||
.AddSigningCredential(Certificate.Get()) | |||
.AddAspNetIdentity<ApplicationUser>() | |||
.AddConfigurationStore(options => | |||
{ | |||
options.ConfigureDbContext = builder => builder.UseSqlServer(connectionString, | |||
sqlServerOptionsAction: sqlOptions => | |||
{ | |||
sqlOptions.MigrationsAssembly(migrationsAssembly); | |||
//Configuring Connection Resiliency: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency | |||
sqlOptions.EnableRetryOnFailure(maxRetryCount: 15, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null); | |||
}); | |||
}) | |||
.AddOperationalStore(options => | |||
{ | |||
options.ConfigureDbContext = builder => builder.UseSqlServer(connectionString, | |||
sqlServerOptionsAction: sqlOptions => | |||
{ | |||
sqlOptions.MigrationsAssembly(migrationsAssembly); | |||
//Configuring Connection Resiliency: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency | |||
sqlOptions.EnableRetryOnFailure(maxRetryCount: 15, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null); | |||
}); | |||
}) | |||
.Services.AddTransient<IProfileService, ProfileService>(); | |||
var container = new ContainerBuilder(); | |||
container.Populate(services); | |||
return new AutofacServiceProvider(container.Build()); | |||
} | |||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. | |||
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) | |||
{ | |||
loggerFactory.AddConsole(Configuration.GetSection("Logging")); | |||
loggerFactory.AddDebug(); | |||
loggerFactory.AddAzureWebAppDiagnostics(); | |||
loggerFactory.AddApplicationInsights(app.ApplicationServices, LogLevel.Trace); | |||
if (env.IsDevelopment()) | |||
{ | |||
app.UseDeveloperExceptionPage(); | |||
app.UseDatabaseErrorPage(); | |||
} | |||
else | |||
{ | |||
app.UseExceptionHandler("/Home/Error"); | |||
} | |||
var pathBase = Configuration["PATH_BASE"]; | |||
if (!string.IsNullOrEmpty(pathBase)) | |||
{ | |||
loggerFactory.CreateLogger("init").LogDebug($"Using PATH BASE '{pathBase}'"); | |||
app.UsePathBase(pathBase); | |||
} | |||
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously | |||
app.Map("/liveness", lapp => lapp.Run(async ctx => ctx.Response.StatusCode = 200)); | |||
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously | |||
app.UseStaticFiles(); | |||
// Make work identity server redirections in Edge and lastest versions of browers. WARN: Not valid in a production environment. | |||
app.Use(async (context, next) => | |||
{ | |||
context.Response.Headers.Add("Content-Security-Policy", "script-src 'unsafe-inline'"); | |||
await next(); | |||
}); | |||
app.UseForwardedHeaders(); | |||
// Adds IdentityServer | |||
app.UseIdentityServer(); | |||
app.UseMvc(routes => | |||
{ | |||
routes.MapRoute( | |||
name: "default", | |||
template: "{controller=Home}/{action=Index}/{id?}"); | |||
}); | |||
} | |||
private void RegisterAppInsights(IServiceCollection services) | |||
{ | |||
services.AddApplicationInsightsTelemetry(Configuration); | |||
var orchestratorType = Configuration.GetValue<string>("OrchestratorType"); | |||
if (orchestratorType?.ToUpper() == "K8S") | |||
{ | |||
// Enable K8s telemetry initializer | |||
services.EnableKubernetes(); | |||
} | |||
if (orchestratorType?.ToUpper() == "SF") | |||
{ | |||
// Enable SF telemetry initializer | |||
services.AddSingleton<ITelemetryInitializer>((serviceProvider) => | |||
new FabricTelemetryInitializer()); | |||
} | |||
} | |||
} | |||
} |
@ -1,25 +0,0 @@ | |||
{ | |||
"ConnectionString": "Server=tcp:127.0.0.1,5433;Database=Microsoft.eShopOnContainers.Services.IdentityDb;User Id=sa;Password=Pass@word;", | |||
"IsClusterEnv": "False", | |||
"MvcClient": "http://localhost:5100", | |||
"SpaClient": "http://localhost:5104", | |||
"XamarinCallback": "http://localhost:5105/xamarincallback", | |||
"UseCustomizationData": false, | |||
"Logging": { | |||
"IncludeScopes": false, | |||
"LogLevel": { | |||
"Default": "Trace", | |||
"System": "Information", | |||
"Microsoft": "Information" | |||
} | |||
}, | |||
"ApplicationInsights": { | |||
"InstrumentationKey": "" | |||
}, | |||
"UseVault": false, | |||
"Vault": { | |||
"Name": "eshop", | |||
"ClientId": "your-clien-id", | |||
"ClientSecret": "your-client-secret" | |||
} | |||
} |
@ -1,25 +0,0 @@ | |||
{ | |||
"ConnectionString": "mongodb://nosql.data", | |||
"Database": "LocationsDb", | |||
"IdentityUrl": "http://localhost:5105", | |||
"Logging": { | |||
"IncludeScopes": false, | |||
"LogLevel": { | |||
"Default": "Trace", | |||
"System": "Information", | |||
"Microsoft": "Information" | |||
} | |||
}, | |||
"AzureServiceBusEnabled": false, | |||
"SubscriptionClientName": "Locations", | |||
"ApplicationInsights": { | |||
"InstrumentationKey": "" | |||
}, | |||
"EventBusRetryCount": 5, | |||
"UseVault": false, | |||
"Vault": { | |||
"Name": "eshop", | |||
"ClientId": "your-clien-id", | |||
"ClientSecret": "your-client-secret" | |||
} | |||
} |
@ -1,26 +0,0 @@ | |||
{ | |||
"Logging": { | |||
"IncludeScopes": false, | |||
"LogLevel": { | |||
"Default": "Trace" | |||
} | |||
}, | |||
"ConnectionString": "Server=tcp:127.0.0.1,5433;Initial Catalog=Microsoft.eShopOnContainers.Services.MarketingDb;User Id=sa;Password=Pass@word", | |||
"MongoConnectionString": "mongodb://nosql.data", | |||
"MongoDatabase": "MarketingDb", | |||
"IdentityUrl": "http://localhost:5105", | |||
"PicBaseUrl": "http://localhost:5110/api/v1/campaigns/[0]/pic/", | |||
"AzureServiceBusEnabled": false, | |||
"SubscriptionClientName": "Marketing", | |||
"AzureStorageEnabled": false, | |||
"ApplicationInsights": { | |||
"InstrumentationKey": "" | |||
}, | |||
"EventBusRetryCount": 5, | |||
"UseVault": false, | |||
"Vault": { | |||
"Name": "eshop", | |||
"ClientId": "your-clien-id", | |||
"ClientSecret": "your-client-secret" | |||
} | |||
} |
@ -1,27 +0,0 @@ | |||
{ | |||
"ConnectionString": "Server=tcp:127.0.0.1,5433;Database=Microsoft.eShopOnContainers.Services.OrderingDb;User Id=sa;Password=Pass@word;", | |||
"IdentityUrl": "http://localhost:5105", | |||
"UseCustomizationData": false, | |||
"Logging": { | |||
"IncludeScopes": false, | |||
"LogLevel": { | |||
"Default": "Trace", | |||
"System": "Information", | |||
"Microsoft": "Information" | |||
} | |||
}, | |||
"AzureServiceBusEnabled": false, | |||
"SubscriptionClientName": "Ordering", | |||
"CheckUpdateTime": "30000", | |||
"ApplicationInsights": { | |||
"InstrumentationKey": "" | |||
}, | |||
"EventBusRetryCount": 5, | |||
"EventBusConnection": "localhost", | |||
"UseVault": false, | |||
"Vault": { | |||
"Name": "eshop", | |||
"ClientId": "your-clien-id", | |||
"ClientSecret": "your-client-secret" | |||
} | |||
} |
@ -1,15 +0,0 @@ | |||
{ | |||
"IdentityUrl": "http://localhost:5105", | |||
"Logging": { | |||
"IncludeScopes": false, | |||
"LogLevel": { | |||
"Default": "Trace", | |||
"System": "Information", | |||
"Microsoft": "Information" | |||
} | |||
}, | |||
"AzureServiceBusEnabled": false, | |||
"SubscriptionClientName": "Ordering.signalrhub", | |||
"EventBusRetryCount": 5, | |||
"EventBusConnection": "localhost" | |||
} |
@ -0,0 +1,16 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Threading.Tasks; | |||
using Microsoft.AspNetCore.Mvc; | |||
namespace WebMVC.Controllers | |||
{ | |||
public class HomeController : Controller | |||
{ | |||
public IActionResult Privacy() | |||
{ | |||
return View(); | |||
} | |||
} | |||
} |
@ -1,29 +0,0 @@ | |||
ARG NODE_IMAGE=node:8.11 | |||
FROM microsoft/dotnet:2.1-aspnetcore-runtime AS base | |||
WORKDIR /app | |||
EXPOSE 80 | |||
FROM microsoft/dotnet:2.1-sdk as dotnet-build | |||
WORKDIR /src | |||
FROM ${NODE_IMAGE} as node-build | |||
WORKDIR /web | |||
COPY src/Web/WebMVC . | |||
RUN npm install -g bower@1.8.4 | |||
RUN bower install --allow-root | |||
FROM dotnet-build as build | |||
WORKDIR /src/src/Web/WebMVC/wwwroot | |||
COPY --from=node-build /web/wwwroot . | |||
WORKDIR /src | |||
COPY . . | |||
WORKDIR /src/src/Web/WebMVC | |||
RUN dotnet restore -nowarn:msb3202,nu1503 | |||
FROM build AS publish | |||
RUN dotnet publish --no-restore -c Release -o /app | |||
FROM base AS final | |||
WORKDIR /app | |||
COPY --from=publish /app . | |||
ENTRYPOINT ["dotnet", "WebMVC.dll"] |
@ -0,0 +1,6 @@ | |||
@{ | |||
ViewData["Title"] = "Privacy Policy"; | |||
} | |||
<h2>@ViewData["Title"]</h2> | |||
<p>Use this page to detail your site's privacy policy.</p> |
@ -0,0 +1,41 @@ | |||
@using Microsoft.AspNetCore.Http.Features | |||
@{ | |||
var consentFeature = Context.Features.Get<ITrackingConsentFeature>(); | |||
var showBanner = !consentFeature?.CanTrack ?? false; | |||
var cookieString = consentFeature?.CreateConsentCookie(); | |||
} | |||
@if (showBanner) | |||
{ | |||
<nav id="cookieConsent" class="navbar navbar-default navbar-fixed-top" role="alert"> | |||
<div class="container"> | |||
<div class="navbar-header"> | |||
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#cookieConsent .navbar-collapse"> | |||
<span class="sr-only">Toggle cookie consent banner</span> | |||
<span class="icon-bar"></span> | |||
<span class="icon-bar"></span> | |||
<span class="icon-bar"></span> | |||
</button> | |||
<span class="navbar-brand"><span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span></span> | |||
</div> | |||
<div class="collapse navbar-collapse"> | |||
<p class="navbar-text"> | |||
Use this space to summarize your privacy and cookie use policy. | |||
</p> | |||
<div class="navbar-right"> | |||
<a asp-controller="Home" asp-action="Privacy" class="btn btn-info navbar-btn">Learn More</a> | |||
<button type="button" class="btn btn-default navbar-btn" data-cookie-string="@cookieString">Accept</button> | |||
</div> | |||
</div> | |||
</div> | |||
</nav> | |||
<script> | |||
(function () { | |||
document.querySelector("#cookieConsent button[data-cookie-string]").addEventListener("click", function (el) { | |||
document.cookie = el.target.dataset.cookieString; | |||
document.querySelector("#cookieConsent").classList.add("hidden"); | |||
}, false); | |||
})(); | |||
</script> | |||
} |
@ -1,27 +0,0 @@ | |||
{ | |||
"CatalogUrl": "http://localhost:5101", | |||
"OrderingUrl": "http://localhost:5102", | |||
"BasketUrl": "http://localhost:5103", | |||
"MarketingUrl": "http://localhost:5110", | |||
"IdentityUrl": "http://localhost:5105", | |||
"CallBackUrl": "http://localhost:5100/", | |||
"LocationsUrl": "http://localhost:5109/", | |||
"IsClusterEnv": "False", | |||
"UseResilientHttp": "True", | |||
"UseLoadTest": false, | |||
"ActivateCampaignDetailFunction": "False", | |||
"UseCustomizationData": false, | |||
"Logging": { | |||
"IncludeScopes": false, | |||
"LogLevel": { | |||
"Default": "Trace", | |||
"System": "Information", | |||
"Microsoft": "Information" | |||
} | |||
}, | |||
"ApplicationInsights": { | |||
"InstrumentationKey": "" | |||
}, | |||
"HttpClientRetryCount": 8, | |||
"HttpClientExceptionsAllowedBeforeBreaking": 7 | |||
} |
@ -1,26 +0,0 @@ | |||
ARG NODE_IMAGE=node:8.11 | |||
FROM microsoft/dotnet:2.1-aspnetcore-runtime AS base | |||
WORKDIR /app | |||
EXPOSE 80 | |||
FROM microsoft/dotnet:2.1-sdk as dotnet-build | |||
WORKDIR /src | |||
FROM ${NODE_IMAGE} as node-build | |||
WORKDIR /web | |||
COPY src/Web/WebSPA . | |||
RUN npm install | |||
RUN npm run build:prod | |||
FROM dotnet-build as publish | |||
WORKDIR /src/src/Web/WebSPA/wwwroot | |||
COPY --from=node-build /web/wwwroot . | |||
WORKDIR /src | |||
COPY . . | |||
WORKDIR /src/src/Web/WebSPA | |||
RUN dotnet publish -c Release -o /app | |||
FROM base AS final | |||
WORKDIR /app | |||
COPY --from=publish /app . | |||
ENTRYPOINT ["dotnet", "WebSPA.dll"] |
@ -1,158 +0,0 @@ | |||
using eShopOnContainers.WebSPA; | |||
using Microsoft.ApplicationInsights.Extensibility; | |||
using Microsoft.ApplicationInsights.ServiceFabric; | |||
using Microsoft.AspNetCore.Antiforgery; | |||
using Microsoft.AspNetCore.Builder; | |||
using Microsoft.AspNetCore.DataProtection; | |||
using Microsoft.AspNetCore.Hosting; | |||
using Microsoft.Extensions.Configuration; | |||
using Microsoft.Extensions.DependencyInjection; | |||
using Microsoft.Extensions.HealthChecks; | |||
using Microsoft.Extensions.Logging; | |||
using Newtonsoft.Json.Serialization; | |||
using StackExchange.Redis; | |||
using System; | |||
using System.IO; | |||
using WebSPA.Infrastructure; | |||
namespace eShopConContainers.WebSPA | |||
{ | |||
public class Startup | |||
{ | |||
public Startup(IConfiguration configuration) | |||
{ | |||
Configuration = configuration; | |||
} | |||
public IConfiguration Configuration { get; } | |||
private IHostingEnvironment _hostingEnv; | |||
public Startup(IHostingEnvironment env) | |||
{ | |||
_hostingEnv = env; | |||
var localPath = new Uri(Configuration["ASPNETCORE_URLS"])?.LocalPath ?? "/"; | |||
Configuration["BaseUrl"] = localPath; | |||
} | |||
// This method gets called by the runtime. Use this method to add services to the container. | |||
// For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=398940 | |||
public void ConfigureServices(IServiceCollection services) | |||
{ | |||
RegisterAppInsights(services); | |||
services.AddHealthChecks(checks => | |||
{ | |||
var minutes = 1; | |||
if (int.TryParse(Configuration["HealthCheck:Timeout"], out var minutesParsed)) | |||
{ | |||
minutes = minutesParsed; | |||
} | |||
checks.AddUrlCheck(Configuration["CatalogUrlHC"], TimeSpan.FromMinutes(minutes)); | |||
checks.AddUrlCheck(Configuration["OrderingUrlHC"], TimeSpan.FromMinutes(minutes)); | |||
checks.AddUrlCheck(Configuration["BasketUrlHC"], TimeSpan.Zero); //No cache for this HealthCheck, better just for demos | |||
checks.AddUrlCheck(Configuration["IdentityUrlHC"], TimeSpan.FromMinutes(minutes)); | |||
checks.AddUrlCheck(Configuration["MarketingUrlHC"], TimeSpan.FromMinutes(minutes)); | |||
}); | |||
services.Configure<AppSettings>(Configuration); | |||
if (Configuration.GetValue<string>("IsClusterEnv") == bool.TrueString) | |||
{ | |||
services.AddDataProtection(opts => | |||
{ | |||
opts.ApplicationDiscriminator = "eshop.webspa"; | |||
}) | |||
.PersistKeysToRedis(ConnectionMultiplexer.Connect(Configuration["DPConnectionString"]), "DataProtection-Keys"); | |||
} | |||
services.AddAntiforgery(options => options.HeaderName = "X-XSRF-TOKEN"); | |||
services.AddMvc() | |||
.AddJsonOptions(options => | |||
{ | |||
options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); | |||
}); | |||
} | |||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. | |||
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IAntiforgery antiforgery) | |||
{ | |||
loggerFactory.AddAzureWebAppDiagnostics(); | |||
loggerFactory.AddApplicationInsights(app.ApplicationServices, LogLevel.Trace); | |||
if (env.IsDevelopment()) | |||
{ | |||
app.UseDeveloperExceptionPage(); | |||
} | |||
// Configure XSRF middleware, This pattern is for SPA style applications where XSRF token is added on Index page | |||
// load and passed back token on every subsequent async request | |||
// app.Use(async (context, next) => | |||
// { | |||
// if (string.Equals(context.Request.Path.Value, "/", StringComparison.OrdinalIgnoreCase)) | |||
// { | |||
// var tokens = antiforgery.GetAndStoreTokens(context); | |||
// context.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken, new CookieOptions() { HttpOnly = false }); | |||
// } | |||
// await next.Invoke(); | |||
// }); | |||
//Seed Data | |||
WebContextSeed.Seed(app, env, loggerFactory); | |||
var pathBase = Configuration["PATH_BASE"]; | |||
if (!string.IsNullOrEmpty(pathBase)) | |||
{ | |||
loggerFactory.CreateLogger("init").LogDebug($"Using PATH BASE '{pathBase}'"); | |||
app.UsePathBase(pathBase); | |||
} | |||
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously | |||
app.Map("/liveness", lapp => lapp.Run(async ctx => ctx.Response.StatusCode = 200)); | |||
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously | |||
app.Use(async (context, next) => | |||
{ | |||
await next(); | |||
// If there's no available file and the request doesn't contain an extension, we're probably trying to access a page. | |||
// Rewrite request to use app root | |||
if (context.Response.StatusCode == 404 && !Path.HasExtension(context.Request.Path.Value) && !context.Request.Path.Value.StartsWith("/api")) | |||
{ | |||
context.Request.Path = "/index.html"; | |||
context.Response.StatusCode = 200; // Make sure we update the status code, otherwise it returns 404 | |||
await next(); | |||
} | |||
}); | |||
app.UseDefaultFiles(); | |||
app.UseStaticFiles(); | |||
app.UseMvcWithDefaultRoute(); | |||
} | |||
private void RegisterAppInsights(IServiceCollection services) | |||
{ | |||
services.AddApplicationInsightsTelemetry(Configuration); | |||
var orchestratorType = Configuration.GetValue<string>("OrchestratorType"); | |||
if (orchestratorType?.ToUpper() == "K8S") | |||
{ | |||
// Enable K8s telemetry initializer | |||
services.EnableKubernetes(); | |||
} | |||
if (orchestratorType?.ToUpper() == "SF") | |||
{ | |||
// Enable SF telemetry initializer | |||
services.AddSingleton<ITelemetryInitializer>((serviceProvider) => | |||
new FabricTelemetryInitializer()); | |||
} | |||
} | |||
} | |||
} |
@ -1,20 +0,0 @@ | |||
{ | |||
"IdentityUrl": "http://localhost:5105", | |||
"MarketingUrl": "http://localhost:5110", | |||
"CallBackUrl": "http://localhost:5104/", | |||
"PurchaseUrl": "http://localhost:5200", | |||
"UseCustomizationData": true, | |||
"IsClusterEnv": "False", | |||
"ActivateCampaignDetailFunction": true, | |||
"Logging": { | |||
"IncludeScopes": false, | |||
"LogLevel": { | |||
"Default": "Debug", | |||
"System": "Information", | |||
"Microsoft": "Information" | |||
} | |||
}, | |||
"ApplicationInsights": { | |||
"InstrumentationKey": "" | |||
} | |||
} |
@ -1,21 +0,0 @@ | |||
{ | |||
"Logging": { | |||
"IncludeScopes": false, | |||
"LogLevel": { | |||
"Default": "Debug", | |||
"System": "Information", | |||
"Microsoft": "Information" | |||
} | |||
}, | |||
"OrderingUrl": "http://localhost:5102/hc", | |||
"OrderingBackgroundTasksUrl": "http://localhost:5111/hc", | |||
"BasketUrl": "http://localhost:5103/hc", | |||
"CatalogUrl": "http://localhost:5101/hc", | |||
"IdentityUrl": "http://localhost:5105/hc", | |||
"MarketingUrl": "http://localhost:5110/hc", | |||
"LocationsUrl": "http://localhost:5109/hc", | |||
"PaymentUrl": "http://localhost:5108/hc", | |||
"ApplicationInsights": { | |||
"InstrumentationKey": "" | |||
} | |||
} |