Browse Source

Merge branch 'features/webhooks' into dev

pull/937/head
eiximenis 6 years ago
parent
commit
0eae3c3ea3
139 changed files with 38664 additions and 36 deletions
  1. +10
    -10
      docker-compose-tests.yml
  2. +25
    -0
      docker-compose.override.yml
  3. +33
    -18
      docker-compose.yml
  4. +105
    -0
      eShopOnContainers-ServicesAndWebApps.sln
  5. +4
    -0
      k8s/helm/app.yaml
  6. +1
    -1
      k8s/helm/deploy-all.ps1
  7. +5
    -0
      k8s/helm/identity-api/templates/configmap.yaml
  8. +4
    -0
      k8s/helm/identity-api/values.yaml
  9. +2
    -0
      k8s/helm/inf.yaml
  10. +21
    -0
      k8s/helm/webhooks-api/.helmignore
  11. +5
    -0
      k8s/helm/webhooks-api/Chart.yaml
  12. +8
    -0
      k8s/helm/webhooks-api/templates/NOTES.txt
  13. +32
    -0
      k8s/helm/webhooks-api/templates/_helpers.tpl
  14. +51
    -0
      k8s/helm/webhooks-api/templates/_names.tpl
  15. +20
    -0
      k8s/helm/webhooks-api/templates/configmap.yaml
  16. +71
    -0
      k8s/helm/webhooks-api/templates/deployment.yaml
  17. +33
    -0
      k8s/helm/webhooks-api/templates/ingress.yaml
  18. +19
    -0
      k8s/helm/webhooks-api/templates/service.yaml
  19. +53
    -0
      k8s/helm/webhooks-api/values.yaml
  20. +21
    -0
      k8s/helm/webhooks-web/.helmignore
  21. +5
    -0
      k8s/helm/webhooks-web/Chart.yaml
  22. +8
    -0
      k8s/helm/webhooks-web/templates/NOTES.txt
  23. +32
    -0
      k8s/helm/webhooks-web/templates/_helpers.tpl
  24. +51
    -0
      k8s/helm/webhooks-web/templates/_names.tpl
  25. +19
    -0
      k8s/helm/webhooks-web/templates/configmap.yaml
  26. +71
    -0
      k8s/helm/webhooks-web/templates/deployment.yaml
  27. +33
    -0
      k8s/helm/webhooks-web/templates/ingress.yaml
  28. +19
    -0
      k8s/helm/webhooks-web/templates/service.yaml
  29. +52
    -0
      k8s/helm/webhooks-web/values.yaml
  30. +1
    -1
      src/BuildingBlocks/WebHostCustomization/WebHost.Customization/WebHost.Customization.csproj
  31. +58
    -6
      src/Services/Identity/Identity.API/Configuration/Config.cs
  32. +2
    -0
      src/Services/Identity/Identity.API/Data/ConfigurationDbContextSeed.cs
  33. +15
    -0
      src/Services/Webhooks/Webhooks.API/Controllers/HomeController.cs
  34. +35
    -0
      src/Services/Webhooks/Webhooks.API/Controllers/WebhookSubscriptionRequest.cs
  35. +115
    -0
      src/Services/Webhooks/Webhooks.API/Controllers/WebhooksController.cs
  36. +19
    -0
      src/Services/Webhooks/Webhooks.API/Dockerfile
  37. +11
    -0
      src/Services/Webhooks/Webhooks.API/Exceptions/WebhooksDomainException.cs
  38. +13
    -0
      src/Services/Webhooks/Webhooks.API/Infrastructure/ActionResult/InternalServerErrorObjectResult.cs
  39. +32
    -0
      src/Services/Webhooks/Webhooks.API/Infrastructure/AuthorizeCheckOperationFilter.cs
  40. +72
    -0
      src/Services/Webhooks/Webhooks.API/Infrastructure/HttpGlobalExceptionFilter.cs
  41. +30
    -0
      src/Services/Webhooks/Webhooks.API/Infrastructure/WebhooksContext.cs
  42. +33
    -0
      src/Services/Webhooks/Webhooks.API/IntegrationEvents/OrderStatusChangedToPaidIntegrationEvent.cs
  43. +32
    -0
      src/Services/Webhooks/Webhooks.API/IntegrationEvents/OrderStatusChangedToPaidIntegrationEventHandler.cs
  44. +22
    -0
      src/Services/Webhooks/Webhooks.API/IntegrationEvents/OrderStatusChangedToShippedIntegrationEvent.cs
  45. +32
    -0
      src/Services/Webhooks/Webhooks.API/IntegrationEvents/OrderStatusChangedToShippedIntegrationEventHandler.cs
  46. +24
    -0
      src/Services/Webhooks/Webhooks.API/IntegrationEvents/ProductPriceChangedIntegrationEvent.cs
  47. +16
    -0
      src/Services/Webhooks/Webhooks.API/IntegrationEvents/ProductPriceChangedIntegrationEventHandler.cs
  48. +47
    -0
      src/Services/Webhooks/Webhooks.API/Migrations/20190118091148_Initial.Designer.cs
  49. +35
    -0
      src/Services/Webhooks/Webhooks.API/Migrations/20190118091148_Initial.cs
  50. +45
    -0
      src/Services/Webhooks/Webhooks.API/Migrations/WebhooksContextModelSnapshot.cs
  51. +26
    -0
      src/Services/Webhooks/Webhooks.API/Model/WebhookData.cs
  52. +18
    -0
      src/Services/Webhooks/Webhooks.API/Model/WebhookSubscription.cs
  53. +14
    -0
      src/Services/Webhooks/Webhooks.API/Model/WebhookType.cs
  54. +27
    -0
      src/Services/Webhooks/Webhooks.API/Program.cs
  55. +32
    -0
      src/Services/Webhooks/Webhooks.API/Properties/launchSettings.json
  56. +57
    -0
      src/Services/Webhooks/Webhooks.API/Services/GrantUrlTesterService.cs
  57. +12
    -0
      src/Services/Webhooks/Webhooks.API/Services/IGrantUrlTesterService.cs
  58. +7
    -0
      src/Services/Webhooks/Webhooks.API/Services/IIdentityService.cs
  59. +14
    -0
      src/Services/Webhooks/Webhooks.API/Services/IWebhooksRetriever.cs
  60. +11
    -0
      src/Services/Webhooks/Webhooks.API/Services/IWebhooksSender.cs
  61. +21
    -0
      src/Services/Webhooks/Webhooks.API/Services/IdentityService.cs
  62. +24
    -0
      src/Services/Webhooks/Webhooks.API/Services/WebhooksRetriever.cs
  63. +49
    -0
      src/Services/Webhooks/Webhooks.API/Services/WebhooksSender.cs
  64. +372
    -0
      src/Services/Webhooks/Webhooks.API/Startup.cs
  65. +32
    -0
      src/Services/Webhooks/Webhooks.API/Webhooks.API.csproj
  66. +10
    -0
      src/Services/Webhooks/Webhooks.API/appsettings.Development.json
  67. +10
    -0
      src/Services/Webhooks/Webhooks.API/appsettings.json
  68. +1
    -0
      src/Web/WebhookClient/.dockerignore
  69. +1
    -0
      src/Web/WebhookClient/.gitignore
  70. +40
    -0
      src/Web/WebhookClient/Controllers/AccountController.cs
  71. +56
    -0
      src/Web/WebhookClient/Controllers/WebhooksReceivedController.cs
  72. +20
    -0
      src/Web/WebhookClient/Dockerfile
  73. +12
    -0
      src/Web/WebhookClient/HeaderNames.cs
  74. +51
    -0
      src/Web/WebhookClient/HttpClientAuthorizationDelegatingHandler.cs
  75. +15
    -0
      src/Web/WebhookClient/Models/WebHookReceived.cs
  76. +16
    -0
      src/Web/WebhookClient/Models/WebhookData.cs
  77. +14
    -0
      src/Web/WebhookClient/Models/WebhookResponse.cs
  78. +15
    -0
      src/Web/WebhookClient/Models/WebhookSubscriptionRequest.cs
  79. +26
    -0
      src/Web/WebhookClient/Pages/Error.cshtml
  80. +23
    -0
      src/Web/WebhookClient/Pages/Error.cshtml.cs
  81. +31
    -0
      src/Web/WebhookClient/Pages/Index.cshtml
  82. +30
    -0
      src/Web/WebhookClient/Pages/Index.cshtml.cs
  83. +8
    -0
      src/Web/WebhookClient/Pages/Privacy.cshtml
  84. +16
    -0
      src/Web/WebhookClient/Pages/Privacy.cshtml.cs
  85. +19
    -0
      src/Web/WebhookClient/Pages/RegisterWebhook.cshtml
  86. +81
    -0
      src/Web/WebhookClient/Pages/RegisterWebhook.cshtml.cs
  87. +79
    -0
      src/Web/WebhookClient/Pages/Shared/_Layout.cshtml
  88. +18
    -0
      src/Web/WebhookClient/Pages/Shared/_ValidationScriptsPartial.cshtml
  89. +28
    -0
      src/Web/WebhookClient/Pages/WebhooksList.cshtml
  90. +28
    -0
      src/Web/WebhookClient/Pages/WebhooksList.cshtml.cs
  91. +3
    -0
      src/Web/WebhookClient/Pages/_ViewImports.cshtml
  92. +3
    -0
      src/Web/WebhookClient/Pages/_ViewStart.cshtml
  93. +24
    -0
      src/Web/WebhookClient/Program.cs
  94. +32
    -0
      src/Web/WebhookClient/Properties/launchSettings.json
  95. +14
    -0
      src/Web/WebhookClient/Services/IHooksRepository.cs
  96. +13
    -0
      src/Web/WebhookClient/Services/IWebhooksClient.cs
  97. +26
    -0
      src/Web/WebhookClient/Services/InMemoryHooksRepository.cs
  98. +30
    -0
      src/Web/WebhookClient/Services/WebhooksClient.cs
  99. +19
    -0
      src/Web/WebhookClient/Settings.cs
  100. +151
    -0
      src/Web/WebhookClient/Startup.cs

+ 10
- 10
docker-compose-tests.yml View File

@ -14,7 +14,7 @@ services:
image: mongo
identity-api-test:
image: eshop/identity-api-test:${TAG:-latest}
image: ${REGISTRY:-eshop}/identity-api-test:${TAG:-latest}
build:
context: .
dockerfile: src/Services/Identity/Identity.API/Dockerfile
@ -22,7 +22,7 @@ services:
- sql-data-test
basket-api-test:
image: eshop/basket-api-test:${TAG:-latest}
image: ${REGISTRY:-eshop}/basket-api-test:${TAG:-latest}
build:
context: .
dockerfile: src/Services/Basket/Basket.API/Dockerfile
@ -35,7 +35,7 @@ services:
- ${BUILD_ARTIFACTSTAGINGDIRECTORY:-./tests-results/}:/tests
basket-api-unit-test:
image: eshop/basket-api-test:${TAG:-latest}
image: ${REGISTRY:-eshop}/basket-api-test:${TAG:-latest}
build:
context: .
dockerfile: src/Services/Basket/Basket.API/Dockerfile
@ -48,7 +48,7 @@ services:
- ${BUILD_ARTIFACTSTAGINGDIRECTORY:-./tests-results/}:/tests
catalog-api-test:
image: eshop/catalog-api-test:${TAG:-latest}
image: ${REGISTRY:-eshop}/catalog-api-test:${TAG:-latest}
build:
context: .
dockerfile: src/Services/Catalog/Catalog.API/Dockerfile
@ -60,7 +60,7 @@ services:
- ${BUILD_ARTIFACTSTAGINGDIRECTORY:-./tests-results/}:/tests
catalog-api-unit-test:
image: eshop/catalog-api-test:${TAG:-latest}
image: ${REGISTRY:-eshop}/catalog-api-test:${TAG:-latest}
build:
context: .
dockerfile: src/Services/Catalog/Catalog.API/Dockerfile
@ -72,7 +72,7 @@ services:
- ${BUILD_ARTIFACTSTAGINGDIRECTORY:-./tests-results/}:/tests
ordering-api-test:
image: eshop/ordering-api-test:${TAG:-latest}
image: ${REGISTRY:-eshop}/ordering-api-test:${TAG:-latest}
build:
context: .
dockerfile: src/Services/Ordering/Ordering.API/Dockerfile
@ -84,7 +84,7 @@ services:
- ${BUILD_ARTIFACTSTAGINGDIRECTORY:-./tests-results/}:/tests
ordering-api-unit-test:
image: eshop/ordering-api-test:${TAG:-latest}
image: ${REGISTRY:-eshop}/ordering-api-test:${TAG:-latest}
build:
context: .
dockerfile: src/Services/Ordering/Ordering.API/Dockerfile
@ -96,7 +96,7 @@ services:
- ${BUILD_ARTIFACTSTAGINGDIRECTORY:-./tests-results/}:/tests
marketing-api-test:
image: eshop/marketing-api-test:${TAG:-latest}
image: ${REGISTRY:-eshop}/marketing-api-test:${TAG:-latest}
build:
context: .
dockerfile: src/Services/Marketing/Marketing.API/Dockerfile
@ -110,7 +110,7 @@ services:
- ${BUILD_ARTIFACTSTAGINGDIRECTORY:-./tests-results/}:/tests
payment-api-test:
image: eshop/payment-api-test:${TAG:-latest}
image: ${REGISTRY:-eshop}/payment-api-test:${TAG:-latest}
build:
context: .
dockerfile: src/Services/Payment/Payment.API/Dockerfile
@ -118,7 +118,7 @@ services:
- rabbitmq-test
locations-api-test:
image: eshop/locations-api-test:${TAG:-latest}
image: ${REGISTRY:-eshop}/locations-api-test:${TAG:-latest}
build:
context: .
dockerfile: src/Services/Location/Locations.API/Dockerfile


+ 25
- 0
docker-compose.override.yml View File

@ -41,6 +41,8 @@ services:
- 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
- WebhooksApiClient=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5113
- WebhooksWebClient=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5114
- UseCustomizationData=True
- ApplicationInsights__InstrumentationKey=${INSTRUMENTATION_KEY}
- OrchestratorType=${ORCHESTRATOR_TYPE}
@ -182,6 +184,19 @@ services:
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).
webhooks.api:
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ASPNETCORE_URLS=http://0.0.0.0:80
- ConnectionString=${ESHOP_AZURE_WEBHOOKS_DB:-Server=sql.data;Database=Microsoft.eShopOnContainers.Services.WebhooksDb;User Id=sa;Password=Pass@word}
- EventBusConnection=${ESHOP_AZURE_SERVICE_BUS:-rabbitmq}
- EventBusUserName=${ESHOP_SERVICE_BUS_USERNAME}
- EventBusPassword=${ESHOP_SERVICE_BUS_PASSWORD}
- IdentityUrl=http://identity.api
- IdentityUrlExternal=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5105
ports:
- "5113: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:
@ -380,3 +395,13 @@ services:
ports:
- "5100:80"
webhooks.client:
environment:
- ASPNETCORE_URLS=http://0.0.0.0:80
- Token=6168DB8D-DC58-4094-AF24-483278923590 # Webhooks are registered with this token (any value is valid) but the client won't check it
- IdentityUrl=http://10.0.75.1:5105
- CallBackUrl=http://localhost:5114
- WebhooksUrl=http://webhooks.api
- SelfUrl=http://webhooks.client/
ports:
- "5114:80"

+ 33
- 18
docker-compose.yml View File

@ -14,7 +14,7 @@ services:
image: rabbitmq:3-management-alpine
identity.api:
image: eshop/identity.api:${TAG:-latest}
image: ${REGISTRY:-eshop}/identity.api:${TAG:-latest}
build:
context: .
dockerfile: src/Services/Identity/Identity.API/Dockerfile
@ -22,7 +22,7 @@ services:
- sql.data
basket.api:
image: eshop/basket.api:${TAG:-latest}
image: ${REGISTRY:-eshop}/basket.api:${TAG:-latest}
build:
context: .
dockerfile: src/Services/Basket/Basket.API/Dockerfile
@ -32,7 +32,7 @@ services:
- rabbitmq
catalog.api:
image: eshop/catalog.api:${TAG:-latest}
image: ${REGISTRY:-eshop}/catalog.api:${TAG:-latest}
build:
context: .
dockerfile: src/Services/Catalog/Catalog.API/Dockerfile
@ -41,7 +41,7 @@ services:
- rabbitmq
ordering.api:
image: eshop/ordering.api:${TAG:-latest}
image: ${REGISTRY:-eshop}/ordering.api:${TAG:-latest}
build:
context: .
dockerfile: src/Services/Ordering/Ordering.API/Dockerfile
@ -50,7 +50,7 @@ services:
- rabbitmq
ordering.backgroundtasks:
image: eshop/ordering.backgroundtasks:${TAG:-latest}
image: ${REGISTRY:-eshop}/ordering.backgroundtasks:${TAG:-latest}
build:
context: .
dockerfile: src/Services/Ordering/Ordering.BackgroundTasks/Dockerfile
@ -59,7 +59,7 @@ services:
- rabbitmq
marketing.api:
image: eshop/marketing.api:${TAG:-latest}
image: ${REGISTRY:-eshop}/marketing.api:${TAG:-latest}
build:
context: .
dockerfile: src/Services/Marketing/Marketing.API/Dockerfile
@ -70,7 +70,7 @@ services:
- rabbitmq
payment.api:
image: eshop/payment.api:${TAG:-latest}
image: ${REGISTRY:-eshop}/payment.api:${TAG:-latest}
build:
context: .
dockerfile: src/Services/Payment/Payment.API/Dockerfile
@ -78,7 +78,7 @@ services:
- rabbitmq
locations.api:
image: eshop/locations.api:${TAG:-latest}
image: ${REGISTRY:-eshop}/locations.api:${TAG:-latest}
build:
context: .
dockerfile: src/Services/Location/Locations.API/Dockerfile
@ -86,8 +86,16 @@ services:
- nosql.data
- rabbitmq
webhooks.api:
image: ${REGISTRY:-eshop}/webhooks.api:${TAG:-latest}
build:
context: .
dockerfile: src/Services/Webhooks/Webhooks.API/Dockerfile
depends_on:
- sql.data
mobileshoppingapigw:
image: eshop/ocelotapigw:${TAG:-latest}
image: ${REGISTRY:-eshop}/ocelotapigw:${TAG:-latest}
build:
context: .
dockerfile: src/ApiGateways/ApiGw-Base/Dockerfile
@ -102,7 +110,7 @@ services:
- basket.api
mobilemarketingapigw:
image: eshop/ocelotapigw:${TAG:-latest}
image: ${REGISTRY:-eshop}/ocelotapigw:${TAG:-latest}
build:
context: .
dockerfile: src/ApiGateways/ApiGw-Base/Dockerfile
@ -117,7 +125,7 @@ services:
- basket.api
webshoppingapigw:
image: eshop/ocelotapigw:${TAG:-latest}
image: ${REGISTRY:-eshop}/ocelotapigw:${TAG:-latest}
build:
context: .
dockerfile: src/ApiGateways/ApiGw-Base/Dockerfile
@ -132,7 +140,7 @@ services:
- basket.api
webmarketingapigw:
image: eshop/ocelotapigw:${TAG:-latest}
image: ${REGISTRY:-eshop}/ocelotapigw:${TAG:-latest}
build:
context: .
dockerfile: src/ApiGateways/ApiGw-Base/Dockerfile
@ -147,7 +155,7 @@ services:
- basket.api
mobileshoppingagg:
image: eshop/mobileshoppingagg:${TAG:-latest}
image: ${REGISTRY:-eshop}/mobileshoppingagg:${TAG:-latest}
build:
context: .
dockerfile: src/ApiGateways/Mobile.Bff.Shopping/aggregator/Dockerfile
@ -162,7 +170,7 @@ services:
- basket.api
webshoppingagg:
image: eshop/webshoppingagg:${TAG:-latest}
image: ${REGISTRY:-eshop}/webshoppingagg:${TAG:-latest}
build:
context: .
dockerfile: src/ApiGateways/Web.Bff.Shopping/aggregator/Dockerfile
@ -177,7 +185,7 @@ services:
- basket.api
ordering.signalrhub:
image: eshop/ordering.signalrhub:${TAG:-latest}
image: ${REGISTRY:-eshop}/ordering.signalrhub:${TAG:-latest}
build:
context: .
dockerfile: src/Services/Ordering/Ordering.SignalrHub/Dockerfile
@ -192,13 +200,13 @@ services:
- basket.api
webstatus:
image: eshop/webstatus:${TAG:-latest}
image: ${REGISTRY:-eshop}/webstatus:${TAG:-latest}
build:
context: .
dockerfile: src/Web/WebStatus/Dockerfile
webspa:
image: eshop/webspa:${TAG:-latest}
image: ${REGISTRY:-eshop}/webspa:${TAG:-latest}
build:
context: .
dockerfile: src/Web/WebSPA/Dockerfile
@ -208,7 +216,7 @@ services:
# - webmarketingapigw
webmvc:
image: eshop/webmvc:${TAG:-latest}
image: ${REGISTRY:-eshop}/webmvc:${TAG:-latest}
build:
context: .
dockerfile: src/Web/WebMVC/Dockerfile
@ -217,3 +225,10 @@ services:
- webshoppingapigw
- webmarketingapigw
webhooks.client:
image: ${REGISTRY:-eshop}/webhooks.client:${TAG:-latest}
build:
context: .
dockerfile: src/Web/WebhookClient/Dockerfile
depends_on:
- webhooks.api

+ 105
- 0
eShopOnContainers-ServicesAndWebApps.sln View File

@ -140,6 +140,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Payment", "Payment", "{C61C
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Payment.API", "src\Services\Payment\Payment.API\Payment.API.csproj", "{0AB40131-8AD7-436F-9C6B-EDA59CFA3A84}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Webhooks", "Webhooks", "{E0AA11C4-2873-461D-8F82-53392530FB7A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Webhooks.API", "src\Services\Webhooks\Webhooks.API\Webhooks.API.csproj", "{84E2016E-0435-44C6-8020-3D288AA38B2C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebhookClient", "src\Web\WebhookClient\WebhookClient.csproj", "{766D7E92-6AF0-476C-ADD5-282BF4D8C576}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Ad-Hoc|Any CPU = Ad-Hoc|Any CPU
@ -1642,6 +1648,102 @@ Global
{0AB40131-8AD7-436F-9C6B-EDA59CFA3A84}.Release|x64.Build.0 = Release|Any CPU
{0AB40131-8AD7-436F-9C6B-EDA59CFA3A84}.Release|x86.ActiveCfg = Release|Any CPU
{0AB40131-8AD7-436F-9C6B-EDA59CFA3A84}.Release|x86.Build.0 = Release|Any CPU
{84E2016E-0435-44C6-8020-3D288AA38B2C}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
{84E2016E-0435-44C6-8020-3D288AA38B2C}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
{84E2016E-0435-44C6-8020-3D288AA38B2C}.Ad-Hoc|ARM.ActiveCfg = Debug|Any CPU
{84E2016E-0435-44C6-8020-3D288AA38B2C}.Ad-Hoc|ARM.Build.0 = Debug|Any CPU
{84E2016E-0435-44C6-8020-3D288AA38B2C}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
{84E2016E-0435-44C6-8020-3D288AA38B2C}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
{84E2016E-0435-44C6-8020-3D288AA38B2C}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{84E2016E-0435-44C6-8020-3D288AA38B2C}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
{84E2016E-0435-44C6-8020-3D288AA38B2C}.Ad-Hoc|x64.ActiveCfg = Debug|Any CPU
{84E2016E-0435-44C6-8020-3D288AA38B2C}.Ad-Hoc|x64.Build.0 = Debug|Any CPU
{84E2016E-0435-44C6-8020-3D288AA38B2C}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU
{84E2016E-0435-44C6-8020-3D288AA38B2C}.Ad-Hoc|x86.Build.0 = Debug|Any CPU
{84E2016E-0435-44C6-8020-3D288AA38B2C}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
{84E2016E-0435-44C6-8020-3D288AA38B2C}.AppStore|Any CPU.Build.0 = Debug|Any CPU
{84E2016E-0435-44C6-8020-3D288AA38B2C}.AppStore|ARM.ActiveCfg = Debug|Any CPU
{84E2016E-0435-44C6-8020-3D288AA38B2C}.AppStore|ARM.Build.0 = Debug|Any CPU
{84E2016E-0435-44C6-8020-3D288AA38B2C}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
{84E2016E-0435-44C6-8020-3D288AA38B2C}.AppStore|iPhone.Build.0 = Debug|Any CPU
{84E2016E-0435-44C6-8020-3D288AA38B2C}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{84E2016E-0435-44C6-8020-3D288AA38B2C}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
{84E2016E-0435-44C6-8020-3D288AA38B2C}.AppStore|x64.ActiveCfg = Debug|Any CPU
{84E2016E-0435-44C6-8020-3D288AA38B2C}.AppStore|x64.Build.0 = Debug|Any CPU
{84E2016E-0435-44C6-8020-3D288AA38B2C}.AppStore|x86.ActiveCfg = Debug|Any CPU
{84E2016E-0435-44C6-8020-3D288AA38B2C}.AppStore|x86.Build.0 = Debug|Any CPU
{84E2016E-0435-44C6-8020-3D288AA38B2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{84E2016E-0435-44C6-8020-3D288AA38B2C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{84E2016E-0435-44C6-8020-3D288AA38B2C}.Debug|ARM.ActiveCfg = Debug|Any CPU
{84E2016E-0435-44C6-8020-3D288AA38B2C}.Debug|ARM.Build.0 = Debug|Any CPU
{84E2016E-0435-44C6-8020-3D288AA38B2C}.Debug|iPhone.ActiveCfg = Debug|Any CPU
{84E2016E-0435-44C6-8020-3D288AA38B2C}.Debug|iPhone.Build.0 = Debug|Any CPU
{84E2016E-0435-44C6-8020-3D288AA38B2C}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{84E2016E-0435-44C6-8020-3D288AA38B2C}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
{84E2016E-0435-44C6-8020-3D288AA38B2C}.Debug|x64.ActiveCfg = Debug|Any CPU
{84E2016E-0435-44C6-8020-3D288AA38B2C}.Debug|x64.Build.0 = Debug|Any CPU
{84E2016E-0435-44C6-8020-3D288AA38B2C}.Debug|x86.ActiveCfg = Debug|Any CPU
{84E2016E-0435-44C6-8020-3D288AA38B2C}.Debug|x86.Build.0 = Debug|Any CPU
{84E2016E-0435-44C6-8020-3D288AA38B2C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{84E2016E-0435-44C6-8020-3D288AA38B2C}.Release|Any CPU.Build.0 = Release|Any CPU
{84E2016E-0435-44C6-8020-3D288AA38B2C}.Release|ARM.ActiveCfg = Release|Any CPU
{84E2016E-0435-44C6-8020-3D288AA38B2C}.Release|ARM.Build.0 = Release|Any CPU
{84E2016E-0435-44C6-8020-3D288AA38B2C}.Release|iPhone.ActiveCfg = Release|Any CPU
{84E2016E-0435-44C6-8020-3D288AA38B2C}.Release|iPhone.Build.0 = Release|Any CPU
{84E2016E-0435-44C6-8020-3D288AA38B2C}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{84E2016E-0435-44C6-8020-3D288AA38B2C}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{84E2016E-0435-44C6-8020-3D288AA38B2C}.Release|x64.ActiveCfg = Release|Any CPU
{84E2016E-0435-44C6-8020-3D288AA38B2C}.Release|x64.Build.0 = Release|Any CPU
{84E2016E-0435-44C6-8020-3D288AA38B2C}.Release|x86.ActiveCfg = Release|Any CPU
{84E2016E-0435-44C6-8020-3D288AA38B2C}.Release|x86.Build.0 = Release|Any CPU
{766D7E92-6AF0-476C-ADD5-282BF4D8C576}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
{766D7E92-6AF0-476C-ADD5-282BF4D8C576}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
{766D7E92-6AF0-476C-ADD5-282BF4D8C576}.Ad-Hoc|ARM.ActiveCfg = Debug|Any CPU
{766D7E92-6AF0-476C-ADD5-282BF4D8C576}.Ad-Hoc|ARM.Build.0 = Debug|Any CPU
{766D7E92-6AF0-476C-ADD5-282BF4D8C576}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
{766D7E92-6AF0-476C-ADD5-282BF4D8C576}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
{766D7E92-6AF0-476C-ADD5-282BF4D8C576}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{766D7E92-6AF0-476C-ADD5-282BF4D8C576}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
{766D7E92-6AF0-476C-ADD5-282BF4D8C576}.Ad-Hoc|x64.ActiveCfg = Debug|Any CPU
{766D7E92-6AF0-476C-ADD5-282BF4D8C576}.Ad-Hoc|x64.Build.0 = Debug|Any CPU
{766D7E92-6AF0-476C-ADD5-282BF4D8C576}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU
{766D7E92-6AF0-476C-ADD5-282BF4D8C576}.Ad-Hoc|x86.Build.0 = Debug|Any CPU
{766D7E92-6AF0-476C-ADD5-282BF4D8C576}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
{766D7E92-6AF0-476C-ADD5-282BF4D8C576}.AppStore|Any CPU.Build.0 = Debug|Any CPU
{766D7E92-6AF0-476C-ADD5-282BF4D8C576}.AppStore|ARM.ActiveCfg = Debug|Any CPU
{766D7E92-6AF0-476C-ADD5-282BF4D8C576}.AppStore|ARM.Build.0 = Debug|Any CPU
{766D7E92-6AF0-476C-ADD5-282BF4D8C576}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
{766D7E92-6AF0-476C-ADD5-282BF4D8C576}.AppStore|iPhone.Build.0 = Debug|Any CPU
{766D7E92-6AF0-476C-ADD5-282BF4D8C576}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{766D7E92-6AF0-476C-ADD5-282BF4D8C576}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
{766D7E92-6AF0-476C-ADD5-282BF4D8C576}.AppStore|x64.ActiveCfg = Debug|Any CPU
{766D7E92-6AF0-476C-ADD5-282BF4D8C576}.AppStore|x64.Build.0 = Debug|Any CPU
{766D7E92-6AF0-476C-ADD5-282BF4D8C576}.AppStore|x86.ActiveCfg = Debug|Any CPU
{766D7E92-6AF0-476C-ADD5-282BF4D8C576}.AppStore|x86.Build.0 = Debug|Any CPU
{766D7E92-6AF0-476C-ADD5-282BF4D8C576}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{766D7E92-6AF0-476C-ADD5-282BF4D8C576}.Debug|Any CPU.Build.0 = Debug|Any CPU
{766D7E92-6AF0-476C-ADD5-282BF4D8C576}.Debug|ARM.ActiveCfg = Debug|Any CPU
{766D7E92-6AF0-476C-ADD5-282BF4D8C576}.Debug|ARM.Build.0 = Debug|Any CPU
{766D7E92-6AF0-476C-ADD5-282BF4D8C576}.Debug|iPhone.ActiveCfg = Debug|Any CPU
{766D7E92-6AF0-476C-ADD5-282BF4D8C576}.Debug|iPhone.Build.0 = Debug|Any CPU
{766D7E92-6AF0-476C-ADD5-282BF4D8C576}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{766D7E92-6AF0-476C-ADD5-282BF4D8C576}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
{766D7E92-6AF0-476C-ADD5-282BF4D8C576}.Debug|x64.ActiveCfg = Debug|Any CPU
{766D7E92-6AF0-476C-ADD5-282BF4D8C576}.Debug|x64.Build.0 = Debug|Any CPU
{766D7E92-6AF0-476C-ADD5-282BF4D8C576}.Debug|x86.ActiveCfg = Debug|Any CPU
{766D7E92-6AF0-476C-ADD5-282BF4D8C576}.Debug|x86.Build.0 = Debug|Any CPU
{766D7E92-6AF0-476C-ADD5-282BF4D8C576}.Release|Any CPU.ActiveCfg = Release|Any CPU
{766D7E92-6AF0-476C-ADD5-282BF4D8C576}.Release|Any CPU.Build.0 = Release|Any CPU
{766D7E92-6AF0-476C-ADD5-282BF4D8C576}.Release|ARM.ActiveCfg = Release|Any CPU
{766D7E92-6AF0-476C-ADD5-282BF4D8C576}.Release|ARM.Build.0 = Release|Any CPU
{766D7E92-6AF0-476C-ADD5-282BF4D8C576}.Release|iPhone.ActiveCfg = Release|Any CPU
{766D7E92-6AF0-476C-ADD5-282BF4D8C576}.Release|iPhone.Build.0 = Release|Any CPU
{766D7E92-6AF0-476C-ADD5-282BF4D8C576}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{766D7E92-6AF0-476C-ADD5-282BF4D8C576}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{766D7E92-6AF0-476C-ADD5-282BF4D8C576}.Release|x64.ActiveCfg = Release|Any CPU
{766D7E92-6AF0-476C-ADD5-282BF4D8C576}.Release|x64.Build.0 = Release|Any CPU
{766D7E92-6AF0-476C-ADD5-282BF4D8C576}.Release|x86.ActiveCfg = Release|Any CPU
{766D7E92-6AF0-476C-ADD5-282BF4D8C576}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -1703,6 +1805,9 @@ Global
{120CABB3-0FEA-4B40-B4B5-2D3041798C80} = {0BD0DB92-2D98-44D9-9AC0-C59186D59B0B}
{C61C5CFE-4876-4A46-A96E-5BBF596A984A} = {91CF7717-08AB-4E65-B10E-0B426F01E2E8}
{0AB40131-8AD7-436F-9C6B-EDA59CFA3A84} = {C61C5CFE-4876-4A46-A96E-5BBF596A984A}
{E0AA11C4-2873-461D-8F82-53392530FB7A} = {91CF7717-08AB-4E65-B10E-0B426F01E2E8}
{84E2016E-0435-44C6-8020-3D288AA38B2C} = {E0AA11C4-2873-461D-8F82-53392530FB7A}
{766D7E92-6AF0-476C-ADD5-282BF4D8C576} = {E279BF0F-7F66-4F3A-A3AB-2CDA66C1CD04}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {25728519-5F0F-4973-8A64-0A81EB4EA8D9}


+ 4
- 0
k8s/helm/app.yaml View File

@ -22,6 +22,8 @@ app: # app global settings
payment: payment-api # ingress entry for payment api
locations: locations-api # ingress entry for locations api
marketing: marketing-api # ingress entry for marketing api
webhooks: webhooks-api # ingress entry for webhooks api
webhooksweb: webhooks-web # ingress entry for webhooks web demo client
svc:
basket: basket # service name for basket api
catalog: catalog # service name for catalog api
@ -41,3 +43,5 @@ app: # app global settings
payment: payment # service name for payment api
locations: locations # service name for locations api
marketing: marketing # service name for marketing ap
webhooks: webhooks # service name for webhooks api
webhooksweb: webhooksweb # service name for webhooks web

+ 1
- 1
k8s/helm/deploy-all.ps1 View File

@ -61,7 +61,7 @@ if (-not [string]::IsNullOrEmpty($registry)) {
Write-Host "Begin eShopOnContainers installation using Helm" -ForegroundColor Green
$infras = ("sql-data", "nosql-data", "rabbitmq", "keystore-data", "basket-data")
$charts = ("eshop-common", "apigwmm", "apigwms", "apigwwm", "apigwws", "basket-api","catalog-api", "identity-api", "locations-api", "marketing-api", "mobileshoppingagg","ordering-api","ordering-backgroundtasks","ordering-signalrhub", "payment-api", "webmvc", "webshoppingagg", "webspa", "webstatus")
$charts = ("eshop-common", "apigwmm", "apigwms", "apigwwm", "apigwws", "basket-api","catalog-api", "identity-api", "locations-api", "marketing-api", "mobileshoppingagg","ordering-api","ordering-backgroundtasks","ordering-signalrhub", "payment-api", "webmvc", "webshoppingagg", "webspa", "webstatus", "webhooks-api", "webhooks-web")
if ($deployInfrastructure) {
foreach ($infra in $infras) {


+ 5
- 0
k8s/helm/identity-api/templates/configmap.yaml View File

@ -9,6 +9,8 @@
{{- $mobileshoppingagg := include "url-of" (list .Values.app.ingress.entries.mobileshoppingagg .) -}}
{{- $webhoppingagg := include "url-of" (list .Values.app.ingress.entries.webshoppingagg .) -}}
{{- $xamarincallback := include "url-of" (list "xamarincallback" .) -}}
{{- $webhooks_url := include "url-of" (list .Values.app.ingress.entries.webhooks .) -}}
{{- $webhooksweb_url := include "url-of" (list .Values.app.ingress.entries.webhooksweb .) -}}
apiVersion: v1
kind: ConfigMap
@ -32,4 +34,7 @@ data:
mobileshoppingagg_e: http://{{ $mobileshoppingagg }}
webshoppingagg_e: http://{{ $webhoppingagg }}
xamarin_callback_e: http://{{ $xamarincallback }}
webhooksapi_e: http://{{ $webhooks_url }}
webhooksweb_e: http://{{ $webhooksweb_url }}

+ 4
- 0
k8s/helm/identity-api/values.yaml View File

@ -54,6 +54,10 @@ env:
key: webshoppingagg_e
- name: XamarinCallback
key: xamarin_callback_e
- name: WebhooksApiClient
key: webhooksapi_e
- name: WebhooksWebClient
key: webhooksweb_e
values:
- name: ASPNETCORE_ENVIRONMENT
value: Development


+ 2
- 0
k8s/helm/inf.yaml View File

@ -16,6 +16,8 @@ inf:
db: IdentityDb # Ordering API SQL db name
marketing:
db: MarketingDb # Marketing API SQL db name
webhooks:
db: WebhooksDb # Webhooks DB
mongo:
# host: my-nosql-data # Uncomment to use specify custom mongo host. By default nosql-data is used
locations:


+ 21
- 0
k8s/helm/webhooks-api/.helmignore View File

@ -0,0 +1,21 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*~
# Various IDEs
.project
.idea/
*.tmproj

+ 5
- 0
k8s/helm/webhooks-api/Chart.yaml View File

@ -0,0 +1,5 @@
apiVersion: v1
appVersion: "1.0"
description: A Helm chart for Kubernetes
name: webhooks-api
version: 0.1.0

+ 8
- 0
k8s/helm/webhooks-api/templates/NOTES.txt View File

@ -0,0 +1,8 @@
eShop Ordering API installed.
-----------------------------
This API is not directly exposed outside cluster. If need to access it use:
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app={{ template "webhooks-api.name" . }},release={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl port-forward $POD_NAME 8080:80

+ 32
- 0
k8s/helm/webhooks-api/templates/_helpers.tpl View File

@ -0,0 +1,32 @@
{{/* vim: set filetype=mustache: */}}
{{/*
Expand the name of the chart.
*/}}
{{- define "webhooks-api.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "webhooks-api.fullname" -}}
{{- if .Values.fullnameOverride -}}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- $name := default .Chart.Name .Values.nameOverride -}}
{{- if contains $name .Release.Name -}}
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "webhooks-api.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
{{- end -}}

+ 51
- 0
k8s/helm/webhooks-api/templates/_names.tpl View File

@ -0,0 +1,51 @@
{{- define "suffix-name" -}}
{{- if .Values.app.name -}}
{{- .Values.app.name -}}
{{- else -}}
{{- .Release.Name -}}
{{- end -}}
{{- end -}}
{{- define "sql-name" -}}
{{- if .Values.inf.sql.host -}}
{{- .Values.inf.sql.host -}}
{{- else -}}
{{- printf "%s" "sql-data" -}}
{{- end -}}
{{- end -}}
{{- define "mongo-name" -}}
{{- if .Values.inf.mongo.host -}}
{{- .Values.inf.mongo.host -}}
{{- else -}}
{{- printf "%s" "nosql-data" -}}
{{- end -}}
{{- end -}}
{{- define "url-of" -}}
{{- $name := first .}}
{{- $ctx := last .}}
{{- if eq $name "" -}}
{{- $ctx.Values.inf.k8s.dns -}}
{{- else -}}
{{- printf "%s/%s" $ctx.Values.inf.k8s.dns $name -}} {{/*Value is just <dns>/<name> */}}
{{- end -}}
{{- end -}}
{{- define "pathBase" -}}
{{- if .Values.inf.k8s.suffix -}}
{{- $suffix := include "suffix-name" . -}}
{{- printf "%s-%s" .Values.pathBase $suffix -}}
{{- else -}}
{{- .Values.pathBase -}}
{{- end -}}
{{- end -}}
{{- define "fqdn-image" -}}
{{- if .Values.inf.registry -}}
{{- printf "%s/%s" .Values.inf.registry.server .Values.image.repository -}}
{{- else -}}
{{- .Values.image.repository -}}
{{- end -}}
{{- end -}}

+ 20
- 0
k8s/helm/webhooks-api/templates/configmap.yaml View File

@ -0,0 +1,20 @@
{{- $name := include "webhooks-api.fullname" . -}}
{{- $sqlsrv := include "sql-name" . -}}
{{- $identity := include "url-of" (list .Values.app.ingress.entries.identity .) -}}
apiVersion: v1
kind: ConfigMap
metadata:
name: "cfg-{{ $name }}"
labels:
app: {{ template "webhooks-api.name" . }}
chart: {{ template "webhooks-api.chart" .}}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
data:
webhooks__ConnectionString: Server={{ $sqlsrv }};Initial Catalog={{ .Values.inf.sql.webhooks.db }};User Id={{ .Values.inf.sql.common.user }};Password={{ .Values.inf.sql.common.pwd }};
urls__IdentityUrl: http://{{ $identity }}
urls__IdentityUrlExternal: http://{{ $identity }}
all__EventBusConnection: {{ .Values.inf.eventbus.constr }}
all__InstrumentationKey: {{ .Values.inf.appinsights.key }}
all__UseAzureServiceBus: "{{ .Values.inf.eventbus.useAzure }}"

+ 71
- 0
k8s/helm/webhooks-api/templates/deployment.yaml View File

@ -0,0 +1,71 @@
{{- $name := include "webhooks-api.fullname" . -}}
{{- $cfgname := printf "%s-%s" "cfg" $name -}}
apiVersion: apps/v1beta2
kind: Deployment
metadata:
name: {{ template "webhooks-api.fullname" . }}
labels:
ufo: {{ $cfgname}}
app: {{ template "webhooks-api.name" . }}
chart: {{ template "webhooks-api.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app: {{ template "webhooks-api.name" . }}
release: {{ .Release.Name }}
template:
metadata:
labels:
app: {{ template "webhooks-api.name" . }}
release: {{ .Release.Name }}
spec:
{{ if .Values.inf.registry -}}
imagePullSecrets:
- name: {{ .Values.inf.registry.secretName }}
{{- end }}
containers:
- name: {{ .Chart.Name }}
image: "{{ template "fqdn-image" . }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
env:
- name: PATH_BASE
value: {{ include "pathBase" . }}
- name: k8sname
value: {{ .Values.clusterName }}
{{- if .Values.env.values -}}
{{- range .Values.env.values }}
- name: {{ .name }}
value: {{ .value | quote }}
{{- end -}}
{{- end -}}
{{- if .Values.env.configmap -}}
{{- range .Values.env.configmap }}
- name: {{ .name }}
valueFrom:
configMapKeyRef:
name: {{ $cfgname }}
key: {{ .key }}
{{- end -}}
{{- end }}
ports:
- name: http
containerPort: 80
protocol: TCP
resources:
{{ toYaml .Values.resources | indent 12 }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{ toYaml . | indent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{ toYaml . | indent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{ toYaml . | indent 8 }}
{{- end }}

+ 33
- 0
k8s/helm/webhooks-api/templates/ingress.yaml View File

@ -0,0 +1,33 @@
{{- if .Values.ingress.enabled -}}
{{- $ingressPath := include "pathBase" . -}}
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: {{ template "webhooks-api.fullname" . }}
labels:
app: {{ template "webhooks-api.name" . }}
chart: {{ template "webhooks-api.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
{{- with .Values.ingress.annotations }}
annotations:
{{ toYaml . | indent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
- {{ .Values.inf.k8s.dns }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
- host: {{ .Values.inf.k8s.dns }}
http:
paths:
- path: {{ $ingressPath }}
backend:
serviceName: {{ .Values.app.svc.webhooks }}
servicePort: http
{{- end }}

+ 19
- 0
k8s/helm/webhooks-api/templates/service.yaml View File

@ -0,0 +1,19 @@
apiVersion: v1
kind: Service
metadata:
name: {{ .Values.app.svc.webhooks }}
labels:
app: {{ template "webhooks-api.name" . }}
chart: {{ template "webhooks-api.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
app: {{ template "webhooks-api.name" . }}
release: {{ .Release.Name }}

+ 53
- 0
k8s/helm/webhooks-api/values.yaml View File

@ -0,0 +1,53 @@
replicaCount: 1
clusterName: eshop-aks
pathBase: /webhooks-api
image:
repository: eshop/webhooks.api
tag: latest
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 80
ingress:
enabled: true
annotations: {}
hosts:
- chart-example.local
tls: []
resources: {}
nodeSelector: {}
tolerations: []
affinity: {}
# env defines the environment variables that will be declared in the pod
env:
urls:
# configmap declares variables which value is taken from the config map defined in template configmap.yaml (name is name of var and key the key in configmap).
configmap:
- name: ConnectionString
key: webhooks__ConnectionString
- name: ApplicationInsights__InstrumentationKey
key: all__InstrumentationKey
- name: EventBusConnection
key: all__EventBusConnection
- name: AzureServiceBusEnabled
key: all__UseAzureServiceBus
- name: IdentityUrl
key: urls__IdentityUrl
- name: IdentityUrlExternal
key: urls__IdentityUrlExternal
# values define environment variables with a fixed value (no configmap involved) (name is name of var, and value is its value)
values:
- name: ASPNETCORE_ENVIRONMENT
value: Development
- name: OrchestratorType
value: 'K8S'

+ 21
- 0
k8s/helm/webhooks-web/.helmignore View File

@ -0,0 +1,21 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*~
# Various IDEs
.project
.idea/
*.tmproj

+ 5
- 0
k8s/helm/webhooks-web/Chart.yaml View File

@ -0,0 +1,5 @@
apiVersion: v1
appVersion: "1.0"
description: A Helm chart for Kubernetes
name: webhooks-web
version: 0.1.0

+ 8
- 0
k8s/helm/webhooks-web/templates/NOTES.txt View File

@ -0,0 +1,8 @@
eShop Ordering API installed.
-----------------------------
This API is not directly exposed outside cluster. If need to access it use:
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app={{ template "webhooks-web.name" . }},release={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl port-forward $POD_NAME 8080:80

+ 32
- 0
k8s/helm/webhooks-web/templates/_helpers.tpl View File

@ -0,0 +1,32 @@
{{/* vim: set filetype=mustache: */}}
{{/*
Expand the name of the chart.
*/}}
{{- define "webhooks-web.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "webhooks-web.fullname" -}}
{{- if .Values.fullnameOverride -}}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- $name := default .Chart.Name .Values.nameOverride -}}
{{- if contains $name .Release.Name -}}
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "webhooks-web.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
{{- end -}}

+ 51
- 0
k8s/helm/webhooks-web/templates/_names.tpl View File

@ -0,0 +1,51 @@
{{- define "suffix-name" -}}
{{- if .Values.app.name -}}
{{- .Values.app.name -}}
{{- else -}}
{{- .Release.Name -}}
{{- end -}}
{{- end -}}
{{- define "sql-name" -}}
{{- if .Values.inf.sql.host -}}
{{- .Values.inf.sql.host -}}
{{- else -}}
{{- printf "%s" "sql-data" -}}
{{- end -}}
{{- end -}}
{{- define "mongo-name" -}}
{{- if .Values.inf.mongo.host -}}
{{- .Values.inf.mongo.host -}}
{{- else -}}
{{- printf "%s" "nosql-data" -}}
{{- end -}}
{{- end -}}
{{- define "url-of" -}}
{{- $name := first .}}
{{- $ctx := last .}}
{{- if eq $name "" -}}
{{- $ctx.Values.inf.k8s.dns -}}
{{- else -}}
{{- printf "%s/%s" $ctx.Values.inf.k8s.dns $name -}} {{/*Value is just <dns>/<name> */}}
{{- end -}}
{{- end -}}
{{- define "pathBase" -}}
{{- if .Values.inf.k8s.suffix -}}
{{- $suffix := include "suffix-name" . -}}
{{- printf "%s-%s" .Values.pathBase $suffix -}}
{{- else -}}
{{- .Values.pathBase -}}
{{- end -}}
{{- end -}}
{{- define "fqdn-image" -}}
{{- if .Values.inf.registry -}}
{{- printf "%s/%s" .Values.inf.registry.server .Values.image.repository -}}
{{- else -}}
{{- .Values.image.repository -}}
{{- end -}}
{{- end -}}

+ 19
- 0
k8s/helm/webhooks-web/templates/configmap.yaml View File

@ -0,0 +1,19 @@
{{- $name := include "webhooks-web.fullname" . -}}
{{- $identity := include "url-of" (list .Values.app.ingress.entries.identity .) -}}
{{- $webhooksweb := include "url-of" (list .Values.app.ingress.entries.webhooksweb .) -}}
{{- $webhooks := include "url-of" (list .Values.app.ingress.entries.webhooks .) -}}
apiVersion: v1
kind: ConfigMap
metadata:
name: "cfg-{{ $name }}"
labels:
app: {{ template "webhooks-web.name" . }}
chart: {{ template "webhooks-web.chart" .}}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
data:
urls__webhooks: http://{{ $webhooks }}
identity_e: http://{{ $identity }}
webhooksweb_e: http://{{ $webhooksweb }}
urls_webhooksweb: http://{{ .Values.app.svc.webhooksweb }}

+ 71
- 0
k8s/helm/webhooks-web/templates/deployment.yaml View File

@ -0,0 +1,71 @@
{{- $name := include "webhooks-web.fullname" . -}}
{{- $cfgname := printf "%s-%s" "cfg" $name -}}
apiVersion: apps/v1beta2
kind: Deployment
metadata:
name: {{ template "webhooks-web.fullname" . }}
labels:
ufo: {{ $cfgname}}
app: {{ template "webhooks-web.name" . }}
chart: {{ template "webhooks-web.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app: {{ template "webhooks-web.name" . }}
release: {{ .Release.Name }}
template:
metadata:
labels:
app: {{ template "webhooks-web.name" . }}
release: {{ .Release.Name }}
spec:
{{ if .Values.inf.registry -}}
imagePullSecrets:
- name: {{ .Values.inf.registry.secretName }}
{{- end }}
containers:
- name: {{ .Chart.Name }}
image: "{{ template "fqdn-image" . }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
env:
- name: PATH_BASE
value: {{ include "pathBase" . }}
- name: k8sname
value: {{ .Values.clusterName }}
{{- if .Values.env.values -}}
{{- range .Values.env.values }}
- name: {{ .name }}
value: {{ .value | quote }}
{{- end -}}
{{- end -}}
{{- if .Values.env.configmap -}}
{{- range .Values.env.configmap }}
- name: {{ .name }}
valueFrom:
configMapKeyRef:
name: {{ $cfgname }}
key: {{ .key }}
{{- end -}}
{{- end }}
ports:
- name: http
containerPort: 80
protocol: TCP
resources:
{{ toYaml .Values.resources | indent 12 }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{ toYaml . | indent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{ toYaml . | indent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{ toYaml . | indent 8 }}
{{- end }}

+ 33
- 0
k8s/helm/webhooks-web/templates/ingress.yaml View File

@ -0,0 +1,33 @@
{{- if .Values.ingress.enabled -}}
{{- $ingressPath := include "pathBase" . -}}
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: {{ template "webhooks-web.fullname" . }}
labels:
app: {{ template "webhooks-web.name" . }}
chart: {{ template "webhooks-web.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
{{- with .Values.ingress.annotations }}
annotations:
{{ toYaml . | indent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
- {{ .Values.inf.k8s.dns }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
- host: {{ .Values.inf.k8s.dns }}
http:
paths:
- path: {{ $ingressPath }}
backend:
serviceName: {{ .Values.app.svc.webhooksweb }}
servicePort: http
{{- end }}

+ 19
- 0
k8s/helm/webhooks-web/templates/service.yaml View File

@ -0,0 +1,19 @@
apiVersion: v1
kind: Service
metadata:
name: {{ .Values.app.svc.webhooksweb }}
labels:
app: {{ template "webhooks-web.name" . }}
chart: {{ template "webhooks-web.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
app: {{ template "webhooks-web.name" . }}
release: {{ .Release.Name }}

+ 52
- 0
k8s/helm/webhooks-web/values.yaml View File

@ -0,0 +1,52 @@
replicaCount: 1
clusterName: eshop-aks
pathBase: /webhooks-web
image:
repository: eshop/webhooks.client
tag: latest
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 80
ingress:
enabled: true
annotations: {}
hosts:
- chart-example.local
tls: []
resources: {}
nodeSelector: {}
tolerations: []
affinity: {}
# env defines the environment variables that will be declared in the pod
env:
urls:
# configmap declares variables which value is taken from the config map defined in template configmap.yaml (name is name of var and key the key in configmap).
configmap:
- name: WebhooksUrl
key: urls__webhooks
- name: IdentityUrl
key: identity_e
- name: CallbackUrl
key: webhooksweb_e
- name: SelfUrl
key: webhooksweb_e
# values define environment variables with a fixed value (no configmap involved) (name is name of var, and value is its value)
values:
- name: ASPNETCORE_ENVIRONMENT
value: Production
- name: OrchestratorType
value: 'K8S'
- name: Token
value: "WebHooks-Demo-Web" # Can use whatever you want

+ 1
- 1
src/BuildingBlocks/WebHostCustomization/WebHost.Customization/WebHost.Customization.csproj View File

@ -6,7 +6,7 @@
<ItemGroup>
<PackageReference Include="Polly" Version="6.0.1" />
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.2.0-preview3-35497" />
<PackageReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
</Project>

+ 58
- 6
src/Services/Identity/Identity.API/Configuration/Config.cs View File

@ -17,7 +17,8 @@ namespace Microsoft.eShopOnContainers.Services.Identity.API.Configuration
new ApiResource("locations", "Locations Service"),
new ApiResource("mobileshoppingagg", "Mobile Shopping Aggregator"),
new ApiResource("webshoppingagg", "Web Shopping Aggregator"),
new ApiResource("orders.signalrhub", "Ordering Signalr Hub")
new ApiResource("orders.signalrhub", "Ordering Signalr Hub"),
new ApiResource("webhooks", "Webhooks registration Service"),
};
}
@ -33,7 +34,7 @@ namespace Microsoft.eShopOnContainers.Services.Identity.API.Configuration
}
// client want to access resources (aka scopes)
public static IEnumerable<Client> GetClients(Dictionary<string,string> clientsUrl)
public static IEnumerable<Client> GetClients(Dictionary<string, string> clientsUrl)
{
return new List<Client>
{
@ -57,7 +58,8 @@ namespace Microsoft.eShopOnContainers.Services.Identity.API.Configuration
"locations",
"marketing",
"webshoppingagg",
"orders.signalrhub"
"orders.signalrhub",
"webhooks"
},
},
new Client
@ -84,7 +86,8 @@ namespace Microsoft.eShopOnContainers.Services.Identity.API.Configuration
"basket",
"locations",
"marketing",
"mobileshoppingagg"
"mobileshoppingagg",
"webhooks"
},
//Allow requesting refresh tokens for long lived API access
AllowOfflineAccess = true,
@ -122,7 +125,40 @@ namespace Microsoft.eShopOnContainers.Services.Identity.API.Configuration
"locations",
"marketing",
"webshoppingagg",
"orders.signalrhub"
"orders.signalrhub",
"webhooks"
},
AccessTokenLifetime = 60*60*2, // 2 hours
IdentityTokenLifetime= 60*60*2 // 2 hours
},
new Client
{
ClientId = "webhooksclient",
ClientName = "Webhooks Client",
ClientSecrets = new List<Secret>
{
new Secret("secret".Sha256())
},
ClientUri = $"{clientsUrl["WebhooksWeb"]}", // public uri of the client
AllowedGrantTypes = GrantTypes.Hybrid,
AllowAccessTokensViaBrowser = false,
RequireConsent = false,
AllowOfflineAccess = true,
AlwaysIncludeUserClaimsInIdToken = true,
RedirectUris = new List<string>
{
$"{clientsUrl["WebhooksWeb"]}/signin-oidc"
},
PostLogoutRedirectUris = new List<string>
{
$"{clientsUrl["WebhooksWeb"]}/signout-callback-oidc"
},
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.OfflineAccess,
"webhooks"
},
AccessTokenLifetime = 60*60*2, // 2 hours
IdentityTokenLifetime= 60*60*2 // 2 hours
@ -157,7 +193,8 @@ namespace Microsoft.eShopOnContainers.Services.Identity.API.Configuration
"basket",
"locations",
"marketing",
"webshoppingagg"
"webshoppingagg",
"webhooks"
},
},
new Client
@ -249,6 +286,21 @@ namespace Microsoft.eShopOnContainers.Services.Identity.API.Configuration
{
"webshoppingagg"
}
},
new Client
{
ClientId = "webhooksswaggerui",
ClientName = "WebHooks Service Swagger UI",
AllowedGrantTypes = GrantTypes.Implicit,
AllowAccessTokensViaBrowser = true,
RedirectUris = { $"{clientsUrl["WebhooksApi"]}/swagger/oauth2-redirect.html" },
PostLogoutRedirectUris = { $"{clientsUrl["WebhooksApi"]}/swagger/" },
AllowedScopes =
{
"webhooks"
}
}
};
}


+ 2
- 0
src/Services/Identity/Identity.API/Data/ConfigurationDbContextSeed.cs View File

@ -28,6 +28,8 @@ namespace Microsoft.eShopOnContainers.Services.Identity.API.Data
clientUrls.Add("OrderingApi", configuration.GetValue<string>("OrderingApiClient"));
clientUrls.Add("MobileShoppingAgg", configuration.GetValue<string>("MobileShoppingAggClient"));
clientUrls.Add("WebShoppingAgg", configuration.GetValue<string>("WebShoppingAggClient"));
clientUrls.Add("WebhooksApi", configuration.GetValue<string>("WebhooksApiClient"));
clientUrls.Add("WebhooksWeb", configuration.GetValue<string>("WebhooksWebClient"));
if (!context.Clients.Any())
{


+ 15
- 0
src/Services/Webhooks/Webhooks.API/Controllers/HomeController.cs View File

@ -0,0 +1,15 @@
using Microsoft.AspNetCore.Mvc;
namespace Webhooks.API.Controllers
{
public class HomeController : Controller
{
// GET: /<controller>/
public IActionResult Index()
{
return new RedirectResult("~/swagger");
}
}
}

+ 35
- 0
src/Services/Webhooks/Webhooks.API/Controllers/WebhookSubscriptionRequest.cs View File

@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Webhooks.API.Model;
namespace Webhooks.API.Controllers
{
public class WebhookSubscriptionRequest : IValidatableObject
{
public string Url { get; set; }
public string Token { get; set; }
public string Event { get; set; }
public string GrantUrl { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (!Uri.IsWellFormedUriString(GrantUrl, UriKind.Absolute))
{
yield return new ValidationResult("GrantUrl is not valid", new[] { nameof(GrantUrl) });
}
if (!Uri.IsWellFormedUriString(Url, UriKind.Absolute))
{
yield return new ValidationResult("Url is not valid", new[] { nameof(Url) });
}
var isOk = Enum.TryParse<WebhookType>(Event, ignoreCase: true, result: out WebhookType whtype);
if (!isOk)
{
yield return new ValidationResult($"{Event} is invalid event name", new[] { nameof(Event) });
}
}
}
}

+ 115
- 0
src/Services/Webhooks/Webhooks.API/Controllers/WebhooksController.cs View File

@ -0,0 +1,115 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Webhooks.API.Infrastructure;
using Webhooks.API.Model;
using Webhooks.API.Services;
namespace Webhooks.API.Controllers
{
[Route("api/v1/[controller]")]
[ApiController]
public class WebhooksController : ControllerBase
{
private readonly WebhooksContext _dbContext;
private readonly IIdentityService _identityService;
private readonly IGrantUrlTesterService _grantUrlTester;
public WebhooksController(WebhooksContext dbContext, IIdentityService identityService, IGrantUrlTesterService grantUrlTester)
{
_dbContext = dbContext;
_identityService = identityService;
_grantUrlTester = grantUrlTester;
}
[Authorize]
[HttpGet]
[ProducesResponseType(typeof(IEnumerable<WebhookSubscription>), (int)HttpStatusCode.OK)]
public async Task<IActionResult> ListByUser()
{
var userId = _identityService.GetUserIdentity();
var data = await _dbContext.Subscriptions.Where(s => s.UserId == userId).ToListAsync();
return Ok(data);
}
[Authorize]
[HttpGet("{id:int}")]
[ProducesResponseType(typeof(WebhookSubscription), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NotFound)]
public async Task<IActionResult> GetByUserAndId(int id)
{
var userId = _identityService.GetUserIdentity();
var subscription = await _dbContext.Subscriptions.SingleOrDefaultAsync(s => s.Id == id && s.UserId == userId);
if (subscription != null)
{
return Ok(subscription);
}
return NotFound($"Subscriptions {id} not found");
}
[Authorize]
[HttpPost]
[ProducesResponseType((int)HttpStatusCode.Created)]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
[ProducesResponseType(418)]
public async Task<IActionResult> SubscribeWebhook(WebhookSubscriptionRequest request)
{
if (!ModelState.IsValid)
{
return ValidationProblem(ModelState);
}
var userId = _identityService.GetUserIdentity();
var grantOk = await _grantUrlTester.TestGrantUrl(request.Url, request.GrantUrl, request.Token ?? string.Empty);
if (grantOk)
{
var subscription = new WebhookSubscription()
{
Date = DateTime.UtcNow,
DestUrl = request.Url,
Token = request.Token,
Type = Enum.Parse<WebhookType>(request.Event, ignoreCase: true),
UserId = _identityService.GetUserIdentity()
};
_dbContext.Add(subscription);
await _dbContext.SaveChangesAsync();
return CreatedAtAction("GetByUserAndId", new { id = subscription.Id }, subscription);
}
else
{
return StatusCode(418, "Grant url can't be validated");
}
}
[Authorize]
[HttpDelete("{id:int}")]
[ProducesResponseType((int)HttpStatusCode.Accepted)]
[ProducesResponseType((int)HttpStatusCode.NotFound)]
public async Task<IActionResult> UnsubscribeWebhook(int id)
{
var userId = _identityService.GetUserIdentity();
var subscription = await _dbContext.Subscriptions.SingleOrDefaultAsync(s => s.Id == id && s.UserId == userId);
if (subscription != null)
{
_dbContext.Remove(subscription);
await _dbContext.SaveChangesAsync();
return Accepted();
}
return NotFound($"Subscriptions {id} not found");
}
}
}

+ 19
- 0
src/Services/Webhooks/Webhooks.API/Dockerfile View File

@ -0,0 +1,19 @@
FROM microsoft/dotnet:2.2-aspnetcore-runtime AS base
WORKDIR /app
EXPOSE 80
FROM microsoft/dotnet:2.2-sdk AS build
WORKDIR /src
COPY ["src/Services/Webhooks/Webhooks.API/Webhooks.API.csproj", "src/Services/Webhooks/Webhooks.API/"]
RUN dotnet restore "src/Services/Webhooks/Webhooks.API/Webhooks.API.csproj"
COPY . .
WORKDIR "/src/src/Services/Webhooks/Webhooks.API"
RUN dotnet build "Webhooks.API.csproj" -c Release -o /app
FROM build AS publish
RUN dotnet publish "Webhooks.API.csproj" -c Release -o /app
FROM base AS final
WORKDIR /app
COPY --from=publish /app .
ENTRYPOINT ["dotnet", "Webhooks.API.dll"]

+ 11
- 0
src/Services/Webhooks/Webhooks.API/Exceptions/WebhooksDomainException.cs View File

@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Webhooks.API.Exceptions
{
public class WebhooksDomainException : Exception
{
}
}

+ 13
- 0
src/Services/Webhooks/Webhooks.API/Infrastructure/ActionResult/InternalServerErrorObjectResult.cs View File

@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Webhooks.API.Infrastructure.ActionResult
{
class InternalServerErrorObjectResult : ObjectResult
{
public InternalServerErrorObjectResult(object error) : base(error)
{
StatusCode = StatusCodes.Status500InternalServerError;
}
}
}

+ 32
- 0
src/Services/Webhooks/Webhooks.API/Infrastructure/AuthorizeCheckOperationFilter.cs View File

@ -0,0 +1,32 @@
using Microsoft.AspNetCore.Authorization;
using Swashbuckle.AspNetCore.Swagger;
using Swashbuckle.AspNetCore.SwaggerGen;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Webhooks.API.Infrastructure
{
public class AuthorizeCheckOperationFilter : IOperationFilter
{
public void Apply(Operation operation, OperationFilterContext context)
{
// Check for authorize attribute
var hasAuthorize = context.ApiDescription.ControllerAttributes().OfType<AuthorizeAttribute>().Any() ||
context.ApiDescription.ActionAttributes().OfType<AuthorizeAttribute>().Any();
if (hasAuthorize)
{
operation.Responses.Add("401", new Response { Description = "Unauthorized" });
operation.Responses.Add("403", new Response { Description = "Forbidden" });
operation.Security = new List<IDictionary<string, IEnumerable<string>>>();
operation.Security.Add(new Dictionary<string, IEnumerable<string>>
{
{ "oauth2", new [] { "webhooksapi" } }
});
}
}
}
}

+ 72
- 0
src/Services/Webhooks/Webhooks.API/Infrastructure/HttpGlobalExceptionFilter.cs View File

@ -0,0 +1,72 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Webhooks.API.Exceptions;
using Webhooks.API.Infrastructure.ActionResult;
namespace Webhooks.API.Infrastructure
{
public class HttpGlobalExceptionFilter : IExceptionFilter
{
private readonly IHostingEnvironment env;
private readonly ILogger<HttpGlobalExceptionFilter> logger;
public HttpGlobalExceptionFilter(IHostingEnvironment env, ILogger<HttpGlobalExceptionFilter> logger)
{
this.env = env;
this.logger = logger;
}
public void OnException(ExceptionContext context)
{
logger.LogError(new EventId(context.Exception.HResult),
context.Exception,
context.Exception.Message);
if (context.Exception.GetType() == typeof(WebhooksDomainException))
{
var problemDetails = new ValidationProblemDetails()
{
Instance = context.HttpContext.Request.Path,
Status = StatusCodes.Status400BadRequest,
Detail = "Please refer to the errors property for additional details."
};
problemDetails.Errors.Add("DomainValidations", new string[] { context.Exception.Message.ToString() });
context.Result = new BadRequestObjectResult(problemDetails);
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest;
}
else
{
var json = new JsonErrorResponse
{
Messages = new[] { "An error ocurred." }
};
if (env.IsDevelopment())
{
json.DeveloperMeesage = context.Exception;
}
context.Result = new InternalServerErrorObjectResult(json);
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
}
context.ExceptionHandled = true;
}
private class JsonErrorResponse
{
public string[] Messages { get; set; }
public object DeveloperMeesage { get; set; }
}
}
}

+ 30
- 0
src/Services/Webhooks/Webhooks.API/Infrastructure/WebhooksContext.cs View File

@ -0,0 +1,30 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Webhooks.API.Model;
namespace Webhooks.API.Infrastructure
{
public class WebhooksContext : DbContext
{
public WebhooksContext(DbContextOptions<WebhooksContext> options) : base(options)
{
}
public DbSet<WebhookSubscription> Subscriptions { get; set; }
}
public class WebhooksContextDesignFactory : IDesignTimeDbContextFactory<WebhooksContext>
{
public WebhooksContext CreateDbContext(string[] args)
{
var optionsBuilder = new DbContextOptionsBuilder<WebhooksContext>()
.UseSqlServer("Server=.;Initial Catalog=Microsoft.eShopOnContainers.Services.CatalogDb;Integrated Security=true");
return new WebhooksContext(optionsBuilder.Options);
}
}
}

+ 33
- 0
src/Services/Webhooks/Webhooks.API/IntegrationEvents/OrderStatusChangedToPaidIntegrationEvent.cs View File

@ -0,0 +1,33 @@
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Webhooks.API.IntegrationEvents
{
public class OrderStatusChangedToPaidIntegrationEvent : IntegrationEvent
{
public int OrderId { get; }
public IEnumerable<OrderStockItem> OrderStockItems { get; }
public OrderStatusChangedToPaidIntegrationEvent(int orderId,
IEnumerable<OrderStockItem> orderStockItems)
{
OrderId = orderId;
OrderStockItems = orderStockItems;
}
}
public class OrderStockItem
{
public int ProductId { get; }
public int Units { get; }
public OrderStockItem(int productId, int units)
{
ProductId = productId;
Units = units;
}
}
}

+ 32
- 0
src/Services/Webhooks/Webhooks.API/IntegrationEvents/OrderStatusChangedToPaidIntegrationEventHandler.cs View File

@ -0,0 +1,32 @@
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Webhooks.API.Model;
using Webhooks.API.Services;
using Microsoft.Extensions.Logging;
namespace Webhooks.API.IntegrationEvents
{
public class OrderStatusChangedToPaidIntegrationEventHandler : IIntegrationEventHandler<OrderStatusChangedToPaidIntegrationEvent>
{
private readonly IWebhooksRetriever _retriever;
private readonly IWebhooksSender _sender;
private readonly ILogger _logger;
public OrderStatusChangedToPaidIntegrationEventHandler(IWebhooksRetriever retriever, IWebhooksSender sender, ILogger<OrderStatusChangedToShippedIntegrationEventHandler> logger )
{
_retriever = retriever;
_sender = sender;
_logger = logger;
}
public async Task Handle(OrderStatusChangedToPaidIntegrationEvent @event)
{
var subscriptions = await _retriever.GetSubscriptionsOfType(WebhookType.OrderPaid);
_logger.LogInformation($"Received OrderStatusChangedToShippedIntegrationEvent and got {subscriptions.Count()} subscriptions to process");
var whook = new WebhookData(WebhookType.OrderPaid, @event);
await _sender.SendAll(subscriptions, whook);
}
}
}

+ 22
- 0
src/Services/Webhooks/Webhooks.API/IntegrationEvents/OrderStatusChangedToShippedIntegrationEvent.cs View File

@ -0,0 +1,22 @@
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Webhooks.API.IntegrationEvents
{
public class OrderStatusChangedToShippedIntegrationEvent : IntegrationEvent
{
public int OrderId { get; private set; }
public string OrderStatus { get; private set; }
public string BuyerName { get; private set; }
public OrderStatusChangedToShippedIntegrationEvent(int orderId, string orderStatus, string buyerName)
{
OrderId = orderId;
OrderStatus = orderStatus;
BuyerName = buyerName;
}
}
}

+ 32
- 0
src/Services/Webhooks/Webhooks.API/IntegrationEvents/OrderStatusChangedToShippedIntegrationEventHandler.cs View File

@ -0,0 +1,32 @@
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Webhooks.API.Model;
using Webhooks.API.Services;
using Microsoft.Extensions.Logging;
namespace Webhooks.API.IntegrationEvents
{
public class OrderStatusChangedToShippedIntegrationEventHandler : IIntegrationEventHandler<OrderStatusChangedToShippedIntegrationEvent>
{
private readonly IWebhooksRetriever _retriever;
private readonly IWebhooksSender _sender;
private readonly ILogger _logger;
public OrderStatusChangedToShippedIntegrationEventHandler(IWebhooksRetriever retriever, IWebhooksSender sender, ILogger<OrderStatusChangedToShippedIntegrationEventHandler> logger )
{
_retriever = retriever;
_sender = sender;
_logger = logger;
}
public async Task Handle(OrderStatusChangedToShippedIntegrationEvent @event)
{
var subscriptions = await _retriever.GetSubscriptionsOfType(WebhookType.OrderShipped);
_logger.LogInformation($"Received OrderStatusChangedToShippedIntegrationEvent and got {subscriptions.Count()} subscriptions to process");
var whook = new WebhookData(WebhookType.OrderShipped, @event);
await _sender.SendAll(subscriptions, whook);
}
}
}

+ 24
- 0
src/Services/Webhooks/Webhooks.API/IntegrationEvents/ProductPriceChangedIntegrationEvent.cs View File

@ -0,0 +1,24 @@
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Webhooks.API.IntegrationEvents
{
public class ProductPriceChangedIntegrationEvent : IntegrationEvent
{
public int ProductId { get; private set; }
public decimal NewPrice { get; private set; }
public decimal OldPrice { get; private set; }
public ProductPriceChangedIntegrationEvent(int productId, decimal newPrice, decimal oldPrice)
{
ProductId = productId;
NewPrice = newPrice;
OldPrice = oldPrice;
}
}
}

+ 16
- 0
src/Services/Webhooks/Webhooks.API/IntegrationEvents/ProductPriceChangedIntegrationEventHandler.cs View File

@ -0,0 +1,16 @@
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Webhooks.API.IntegrationEvents
{
public class ProductPriceChangedIntegrationEventHandler : IIntegrationEventHandler<ProductPriceChangedIntegrationEvent>
{
public async Task Handle(ProductPriceChangedIntegrationEvent @event)
{
int i = 0;
}
}
}

+ 47
- 0
src/Services/Webhooks/Webhooks.API/Migrations/20190118091148_Initial.Designer.cs View File

@ -0,0 +1,47 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Webhooks.API.Infrastructure;
namespace Webhooks.API.Migrations
{
[DbContext(typeof(WebhooksContext))]
[Migration("20190118091148_Initial")]
partial class Initial
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.2.1-servicing-10028")
.HasAnnotation("Relational:MaxIdentifierLength", 128)
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
modelBuilder.Entity("Webhooks.API.Model.WebhookSubscription", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<DateTime>("Date");
b.Property<string>("DestUrl");
b.Property<string>("Token");
b.Property<int>("Type");
b.Property<string>("UserId");
b.HasKey("Id");
b.ToTable("Subscriptions");
});
#pragma warning restore 612, 618
}
}
}

+ 35
- 0
src/Services/Webhooks/Webhooks.API/Migrations/20190118091148_Initial.cs View File

@ -0,0 +1,35 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
namespace Webhooks.API.Migrations
{
public partial class Initial : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Subscriptions",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
Type = table.Column<int>(nullable: false),
Date = table.Column<DateTime>(nullable: false),
DestUrl = table.Column<string>(nullable: true),
Token = table.Column<string>(nullable: true),
UserId = table.Column<string>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Subscriptions", x => x.Id);
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Subscriptions");
}
}
}

+ 45
- 0
src/Services/Webhooks/Webhooks.API/Migrations/WebhooksContextModelSnapshot.cs View File

@ -0,0 +1,45 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Webhooks.API.Infrastructure;
namespace Webhooks.API.Migrations
{
[DbContext(typeof(WebhooksContext))]
partial class WebhooksContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.2.1-servicing-10028")
.HasAnnotation("Relational:MaxIdentifierLength", 128)
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
modelBuilder.Entity("Webhooks.API.Model.WebhookSubscription", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<DateTime>("Date");
b.Property<string>("DestUrl");
b.Property<string>("Token");
b.Property<int>("Type");
b.Property<string>("UserId");
b.HasKey("Id");
b.ToTable("Subscriptions");
});
#pragma warning restore 612, 618
}
}
}

+ 26
- 0
src/Services/Webhooks/Webhooks.API/Model/WebhookData.cs View File

@ -0,0 +1,26 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Webhooks.API.Model
{
public class WebhookData
{
public DateTime When { get; }
public string Payload { get; }
public string Type { get; }
public WebhookData(WebhookType hookType, object data)
{
When = DateTime.UtcNow;
Type = hookType.ToString();
Payload = JsonConvert.SerializeObject(data);
}
}
}

+ 18
- 0
src/Services/Webhooks/Webhooks.API/Model/WebhookSubscription.cs View File

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Webhooks.API.Model
{
public class WebhookSubscription
{
public int Id { get; set; }
public WebhookType Type { get; set; }
public DateTime Date { get; set; }
public string DestUrl { get; set; }
public string Token { get; set; }
public string UserId { get; set; }
}
}

+ 14
- 0
src/Services/Webhooks/Webhooks.API/Model/WebhookType.cs View File

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Webhooks.API.Model
{
public enum WebhookType
{
CatalogItemPriceChange = 1,
OrderShipped = 2,
OrderPaid = 3
}
}

+ 27
- 0
src/Services/Webhooks/Webhooks.API/Program.cs View File

@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Webhooks.API.Infrastructure;
namespace Webhooks.API
{
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build()
.MigrateDbContext<WebhooksContext>((_,__) => { })
.Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}
}

+ 32
- 0
src/Services/Webhooks/Webhooks.API/Properties/launchSettings.json View File

@ -0,0 +1,32 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:62486",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Webhooks.API": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:5000"
},
"Docker": {
"commandName": "Docker",
"launchBrowser": true,
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}"
}
}
}

+ 57
- 0
src/Services/Webhooks/Webhooks.API/Services/GrantUrlTesterService.cs View File

@ -0,0 +1,57 @@
using Microsoft.Extensions.Logging;
using System;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
namespace Webhooks.API.Services
{
class GrantUrlTesterService : IGrantUrlTesterService
{
private readonly IHttpClientFactory _clientFactory;
private readonly ILogger _logger;
public GrantUrlTesterService(IHttpClientFactory factory, ILogger<IGrantUrlTesterService> logger)
{
_clientFactory = factory;
_logger = logger;
}
public async Task<bool> TestGrantUrl(string urlHook, string url, string token)
{
if (!CheckSameOrigin(urlHook, url))
{
_logger.LogWarning($"Url of the hook ({urlHook} and the grant url ({url} do not belong to same origin)");
return false;
}
var client = _clientFactory.CreateClient("GrantClient");
var msg = new HttpRequestMessage(HttpMethod.Options, url);
msg.Headers.Add("X-eshop-whtoken", token);
_logger.LogInformation($"Sending the OPTIONS message to {url} with token {token ?? string.Empty}");
try
{
var response = await client.SendAsync(msg);
var tokenReceived = response.Headers.TryGetValues("X-eshop-whtoken", out var tokenValues) ? tokenValues.FirstOrDefault() : null;
var tokenExpected = string.IsNullOrWhiteSpace(token) ? null : token;
_logger.LogInformation($"Response code is {response.StatusCode} for url {url} and token in header was {tokenReceived} (expected token was {tokenExpected})");
return response.IsSuccessStatusCode && tokenReceived == tokenExpected;
}
catch (Exception ex)
{
_logger.LogWarning($"Exception {ex.GetType().Name} when sending OPTIONS request. Url can't be granted.");
return false;
}
}
private bool CheckSameOrigin(string urlHook, string url)
{
var firstUrl = new Uri(urlHook, UriKind.Absolute);
var secondUrl = new Uri(url, UriKind.Absolute);
return firstUrl.Scheme == secondUrl.Scheme &&
firstUrl.Port == secondUrl.Port &&
firstUrl.Host == firstUrl.Host;
}
}
}

+ 12
- 0
src/Services/Webhooks/Webhooks.API/Services/IGrantUrlTesterService.cs View File

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Webhooks.API.Services
{
public interface IGrantUrlTesterService
{
Task<bool> TestGrantUrl(string urlHook, string url, string token);
}
}

+ 7
- 0
src/Services/Webhooks/Webhooks.API/Services/IIdentityService.cs View File

@ -0,0 +1,7 @@
namespace Webhooks.API.Services
{
public interface IIdentityService
{
string GetUserIdentity();
}
}

+ 14
- 0
src/Services/Webhooks/Webhooks.API/Services/IWebhooksRetriever.cs View File

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Webhooks.API.Model;
namespace Webhooks.API.Services
{
public interface IWebhooksRetriever
{
Task<IEnumerable<WebhookSubscription>> GetSubscriptionsOfType(WebhookType type);
}
}

+ 11
- 0
src/Services/Webhooks/Webhooks.API/Services/IWebhooksSender.cs View File

@ -0,0 +1,11 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Webhooks.API.Model;
namespace Webhooks.API.Services
{
public interface IWebhooksSender
{
Task SendAll(IEnumerable<WebhookSubscription> receivers, WebhookData data);
}
}

+ 21
- 0
src/Services/Webhooks/Webhooks.API/Services/IdentityService.cs View File

@ -0,0 +1,21 @@

using Microsoft.AspNetCore.Http;
using System;
namespace Webhooks.API.Services
{
public class IdentityService : IIdentityService
{
private IHttpContextAccessor _context;
public IdentityService(IHttpContextAccessor context)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
}
public string GetUserIdentity()
{
return _context.HttpContext.User.FindFirst("sub").Value;
}
}
}

+ 24
- 0
src/Services/Webhooks/Webhooks.API/Services/WebhooksRetriever.cs View File

@ -0,0 +1,24 @@
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Webhooks.API.Infrastructure;
using Webhooks.API.Model;
namespace Webhooks.API.Services
{
public class WebhooksRetriever : IWebhooksRetriever
{
private readonly WebhooksContext _db;
public WebhooksRetriever(WebhooksContext db)
{
_db = db;
}
public async Task<IEnumerable<WebhookSubscription>> GetSubscriptionsOfType(WebhookType type)
{
var data = await _db.Subscriptions.Where(s => s.Type == type).ToListAsync();
return data;
}
}
}

+ 49
- 0
src/Services/Webhooks/Webhooks.API/Services/WebhooksSender.cs View File

@ -0,0 +1,49 @@
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Webhooks.API.Model;
namespace Webhooks.API.Services
{
public class WebhooksSender : IWebhooksSender
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger _logger;
public WebhooksSender(IHttpClientFactory httpClientFactory, ILogger<WebhooksSender> logger)
{
_httpClientFactory = httpClientFactory;
_logger = logger;
}
public async Task SendAll(IEnumerable<WebhookSubscription> receivers, WebhookData data)
{
var client = _httpClientFactory.CreateClient();
var json = JsonConvert.SerializeObject(data);
var tasks = receivers.Select(r => OnSendData(r, json, client));
await Task.WhenAll(tasks.ToArray());
}
private Task OnSendData(WebhookSubscription subs, string jsonData, HttpClient client)
{
var request = new HttpRequestMessage()
{
RequestUri = new Uri(subs.DestUrl, UriKind.Absolute),
Method = HttpMethod.Post,
Content = new StringContent(jsonData, Encoding.UTF8, "application/json")
};
if (!string.IsNullOrWhiteSpace(subs.Token))
{
request.Headers.Add("X-eshop-whtoken", subs.Token);
}
_logger.LogDebug($"Sending hook to {subs.DestUrl} of type {subs.Type.ToString()}");
return client.SendAsync(request);
}
}
}

+ 372
- 0
src/Services/Webhooks/Webhooks.API/Startup.cs View File

@ -0,0 +1,372 @@
using System;
using System.Collections.Generic;
using System.Data.Common;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Autofac;
using Autofac.Extensions.DependencyInjection;
using HealthChecks.UI.Client;
using Microsoft.ApplicationInsights.Extensibility;
using Microsoft.ApplicationInsights.ServiceFabric;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.ServiceBus;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus;
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions;
using Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ;
using Microsoft.eShopOnContainers.BuildingBlocks.EventBusServiceBus;
using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Services;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Logging;
using RabbitMQ.Client;
using Swashbuckle.AspNetCore.Swagger;
using Webhooks.API.Infrastructure;
using Webhooks.API.IntegrationEvents;
using Webhooks.API.Services;
namespace Webhooks.API
{
public class Startup
{
public IConfiguration Configuration { get; }
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services
.AddAppInsight(Configuration)
.AddCustomMVC(Configuration)
.AddCustomDbContext(Configuration)
.AddSwagger(Configuration)
.AddCustomHealthCheck(Configuration)
.AddHttpClientServices(Configuration)
.AddIntegrationServices(Configuration)
.AddEventBus(Configuration)
.AddCustomAuthentication(Configuration)
.AddSingleton<IHttpContextAccessor, HttpContextAccessor>()
.AddTransient<IIdentityService, IdentityService>()
.AddTransient<IGrantUrlTesterService, GrantUrlTesterService>()
.AddTransient<IWebhooksRetriever, WebhooksRetriever>()
.AddTransient<IWebhooksSender, WebhooksSender>();
var container = new ContainerBuilder();
container.Populate(services);
return new AutofacServiceProvider(container.Build());
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddAzureWebAppDiagnostics();
loggerFactory.AddApplicationInsights(app.ApplicationServices, LogLevel.Trace);
var pathBase = Configuration["PATH_BASE"];
if (!string.IsNullOrEmpty(pathBase))
{
loggerFactory.CreateLogger("init").LogDebug($"Using PATH BASE '{pathBase}'");
app.UsePathBase(pathBase);
}
app.UseHealthChecks("/hc", new HealthCheckOptions()
{
Predicate = _ => true,
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
app.UseHealthChecks("/liveness", new HealthCheckOptions
{
Predicate = r => r.Name.Contains("self")
});
app.UseCors("CorsPolicy");
ConfigureAuth(app);
app.UseMvcWithDefaultRoute();
app.UseSwagger()
.UseSwaggerUI(c =>
{
c.SwaggerEndpoint($"{ (!string.IsNullOrEmpty(pathBase) ? pathBase : string.Empty) }/swagger/v1/swagger.json", "Webhooks.API V1");
c.OAuthClientId("webhooksswaggerui");
c.OAuthAppName("WebHooks Service Swagger UI");
});
ConfigureEventBus(app);
}
protected virtual void ConfigureAuth(IApplicationBuilder app)
{
/*
if (Configuration.GetValue<bool>("UseLoadTest"))
{
app.UseMiddleware<ByPassAuthMiddleware>();
}
*/
app.UseAuthentication();
}
protected virtual void ConfigureEventBus(IApplicationBuilder app)
{
var eventBus = app.ApplicationServices.GetRequiredService<IEventBus>();
eventBus.Subscribe<ProductPriceChangedIntegrationEvent, ProductPriceChangedIntegrationEventHandler>();
eventBus.Subscribe<OrderStatusChangedToShippedIntegrationEvent, OrderStatusChangedToShippedIntegrationEventHandler>();
eventBus.Subscribe<OrderStatusChangedToPaidIntegrationEvent, OrderStatusChangedToPaidIntegrationEventHandler>();
}
}
static class CustomExtensionMethods
{
public static IServiceCollection AddAppInsight(this IServiceCollection services, IConfiguration configuration)
{
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());
}
return services;
}
public static IServiceCollection AddCustomMVC(this IServiceCollection services, IConfiguration configuration)
{
services.AddMvc(options =>
{
options.Filters.Add(typeof(HttpGlobalExceptionFilter));
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
.AddControllersAsServices();
services.AddCors(options =>
{
options.AddPolicy("CorsPolicy",
builder => builder
.SetIsOriginAllowed((host) => true)
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials());
});
return services;
}
public static IServiceCollection AddCustomDbContext(this IServiceCollection services, IConfiguration configuration)
{
services.AddDbContext<WebhooksContext>(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: 10, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null);
});
// Changing default behavior when client evaluation occurs to throw.
// Default in EF Core would be to log a warning when client evaluation is performed.
options.ConfigureWarnings(warnings => warnings.Throw(RelationalEventId.QueryClientEvaluationWarning));
//Check Client vs. Server evaluation: https://docs.microsoft.com/en-us/ef/core/querying/client-eval
});
return services;
}
public static IServiceCollection AddSwagger(this IServiceCollection services, IConfiguration configuration)
{
services.AddSwaggerGen(options =>
{
options.DescribeAllEnumsAsStrings();
options.SwaggerDoc("v1", new Swashbuckle.AspNetCore.Swagger.Info
{
Title = "eShopOnContainers - Webhooks HTTP API",
Version = "v1",
Description = "The Webhooks Microservice HTTP API. This is a simple webhooks CRUD registration entrypoint",
TermsOfService = "Terms Of Service"
});
options.AddSecurityDefinition("oauth2", new OAuth2Scheme
{
Type = "oauth2",
Flow = "implicit",
AuthorizationUrl = $"{configuration.GetValue<string>("IdentityUrlExternal")}/connect/authorize",
TokenUrl = $"{configuration.GetValue<string>("IdentityUrlExternal")}/connect/token",
Scopes = new Dictionary<string, string>()
{
{ "webhooks", "Webhooks API" }
}
});
options.OperationFilter<AuthorizeCheckOperationFilter>();
});
return services;
}
public static IServiceCollection AddEventBus(this IServiceCollection services, IConfiguration configuration)
{
var subscriptionClientName = configuration["SubscriptionClientName"];
if (configuration.GetValue<bool>("AzureServiceBusEnabled"))
{
services.AddSingleton<IEventBus, EventBusServiceBus>(sp =>
{
var serviceBusPersisterConnection = sp.GetRequiredService<IServiceBusPersisterConnection>();
var iLifetimeScope = sp.GetRequiredService<ILifetimeScope>();
var logger = sp.GetRequiredService<ILogger<EventBusServiceBus>>();
var eventBusSubcriptionsManager = sp.GetRequiredService<IEventBusSubscriptionsManager>();
return new EventBusServiceBus(serviceBusPersisterConnection, logger,
eventBusSubcriptionsManager, subscriptionClientName, iLifetimeScope);
});
}
else
{
services.AddSingleton<IEventBus, EventBusRabbitMQ>(sp =>
{
var rabbitMQPersistentConnection = sp.GetRequiredService<IRabbitMQPersistentConnection>();
var iLifetimeScope = sp.GetRequiredService<ILifetimeScope>();
var logger = sp.GetRequiredService<ILogger<EventBusRabbitMQ>>();
var eventBusSubcriptionsManager = sp.GetRequiredService<IEventBusSubscriptionsManager>();
var retryCount = 5;
if (!string.IsNullOrEmpty(configuration["EventBusRetryCount"]))
{
retryCount = int.Parse(configuration["EventBusRetryCount"]);
}
return new EventBusRabbitMQ(rabbitMQPersistentConnection, logger, iLifetimeScope, eventBusSubcriptionsManager, subscriptionClientName, retryCount);
});
}
services.AddSingleton<IEventBusSubscriptionsManager, InMemoryEventBusSubscriptionsManager>();
services.AddTransient<ProductPriceChangedIntegrationEventHandler>();
services.AddTransient<OrderStatusChangedToShippedIntegrationEventHandler>();
services.AddTransient<OrderStatusChangedToPaidIntegrationEventHandler>();
return services;
}
public static IServiceCollection AddCustomHealthCheck(this IServiceCollection services, IConfiguration configuration)
{
var accountName = configuration.GetValue<string>("AzureStorageAccountName");
var accountKey = configuration.GetValue<string>("AzureStorageAccountKey");
var hcBuilder = services.AddHealthChecks();
hcBuilder
.AddCheck("self", () => HealthCheckResult.Healthy())
.AddSqlServer(
configuration["ConnectionString"],
name: "WebhooksApiDb-check",
tags: new string[] { "webhooksdb" });
return services;
}
public static IServiceCollection AddHttpClientServices(this IServiceCollection services, IConfiguration configuration)
{
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddHttpClient("extendedhandlerlifetime").SetHandlerLifetime(Timeout.InfiniteTimeSpan);
//add http client services
services.AddHttpClient("GrantClient")
.SetHandlerLifetime(TimeSpan.FromMinutes(5));
//.AddHttpMessageHandler<HttpClientAuthorizationDelegatingHandler>();
return services;
}
public static IServiceCollection AddIntegrationServices(this IServiceCollection services, IConfiguration configuration)
{
services.AddTransient<Func<DbConnection, IIntegrationEventLogService>>(
sp => (DbConnection c) => new IntegrationEventLogService(c));
if (configuration.GetValue<bool>("AzureServiceBusEnabled"))
{
services.AddSingleton<IServiceBusPersisterConnection>(sp =>
{
var logger = sp.GetRequiredService<ILogger<DefaultServiceBusPersisterConnection>>();
var serviceBusConnection = new ServiceBusConnectionStringBuilder(configuration["EventBusConnection"]);
return new DefaultServiceBusPersisterConnection(serviceBusConnection, logger);
});
}
else
{
services.AddSingleton<IRabbitMQPersistentConnection>(sp =>
{
var logger = sp.GetRequiredService<ILogger<DefaultRabbitMQPersistentConnection>>();
var factory = new ConnectionFactory()
{
HostName = configuration["EventBusConnection"]
};
if (!string.IsNullOrEmpty(configuration["EventBusUserName"]))
{
factory.UserName = configuration["EventBusUserName"];
}
if (!string.IsNullOrEmpty(configuration["EventBusPassword"]))
{
factory.Password = configuration["EventBusPassword"];
}
var retryCount = 5;
if (!string.IsNullOrEmpty(configuration["EventBusRetryCount"]))
{
retryCount = int.Parse(configuration["EventBusRetryCount"]);
}
return new DefaultRabbitMQPersistentConnection(factory, logger, retryCount);
});
}
return services;
}
public static IServiceCollection AddCustomAuthentication(this IServiceCollection services, IConfiguration configuration)
{
// prevent from mapping "sub" claim to nameidentifier.
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("sub");
var identityUrl = configuration.GetValue<string>("IdentityUrl");
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.Authority = identityUrl;
options.RequireHttpsMetadata = false;
options.Audience = "webhooks";
});
return services;
}
}
}

+ 32
- 0
src/Services/Webhooks/Webhooks.API/Webhooks.API.csproj View File

@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
<AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="2.2.0" PrivateAssets="All" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.0.2105168" />
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="4.2.1" />
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.2.1" />
<PackageReference Include="Microsoft.ApplicationInsights.DependencyCollector" Version="2.6.1" />
<PackageReference Include="Microsoft.ApplicationInsights.Kubernetes" Version="1.0.0-beta8" />
<PackageReference Include="Microsoft.ApplicationInsights.ServiceFabric" Version="2.1.1-beta1" />
<PackageReference Include="Microsoft.Extensions.Logging.AzureAppServices" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.HealthChecks" Version="1.0.0" />
<PackageReference Include="AspNetCore.HealthChecks.UI.Client" Version="2.2.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="3.0.0" />
<PackageReference Include="AspNetCore.HealthChecks.SqlServer" Version="2.2.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\BuildingBlocks\EventBus\EventBusRabbitMQ\EventBusRabbitMQ.csproj" />
<ProjectReference Include="..\..\..\BuildingBlocks\EventBus\EventBusServiceBus\EventBusServiceBus.csproj" />
<ProjectReference Include="..\..\..\BuildingBlocks\EventBus\EventBus\EventBus.csproj" />
<ProjectReference Include="..\..\..\BuildingBlocks\EventBus\IntegrationEventLogEF\IntegrationEventLogEF.csproj" />
<ProjectReference Include="..\..\..\BuildingBlocks\WebHostCustomization\WebHost.Customization\WebHost.Customization.csproj" />
</ItemGroup>
</Project>

+ 10
- 0
src/Services/Webhooks/Webhooks.API/appsettings.Development.json View File

@ -0,0 +1,10 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
},
"ConnectionString": "Server=tcp:127.0.0.1,5433;Initial Catalog=Microsoft.eShopOnContainers.Services.CatalogDb;User Id=sa;Password=Pass@word"
}

+ 10
- 0
src/Services/Webhooks/Webhooks.API/appsettings.json View File

@ -0,0 +1,10 @@
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*",
"SubscriptionClientName": "Webhooks",
"EventBusRetryCount": 5
}

+ 1
- 0
src/Web/WebhookClient/.dockerignore View File

@ -0,0 +1 @@
!wwwroot

+ 1
- 0
src/Web/WebhookClient/.gitignore View File

@ -0,0 +1 @@
!wwwroot/lib

+ 40
- 0
src/Web/WebhookClient/Controllers/AccountController.cs View File

@ -0,0 +1,40 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
namespace WebhookClient.Controllers
{
[Authorize]
public class AccountController : Controller
{
public async Task<IActionResult> SignIn(string returnUrl)
{
var user = User as ClaimsPrincipal;
var token = await HttpContext.GetTokenAsync("access_token");
if (token != null)
{
ViewData["access_token"] = token;
}
return RedirectToPage("/Index");
}
public async Task<IActionResult> Signout()
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
await HttpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme);
var homeUrl = Url.Page("/Index");
return new SignOutResult(OpenIdConnectDefaults.AuthenticationScheme,
new AuthenticationProperties { RedirectUri = homeUrl });
}
}
}

+ 56
- 0
src/Web/WebhookClient/Controllers/WebhooksReceivedController.cs View File

@ -0,0 +1,56 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using WebhookClient.Models;
using WebhookClient.Services;
namespace WebhookClient.Controllers
{
[ApiController]
[Route("webhook-received")]
public class WebhooksReceivedController : Controller
{
private readonly Settings _settings;
private readonly ILogger _logger;
private readonly IHooksRepository _hooksRepository;
public WebhooksReceivedController(IOptions<Settings> settings, ILogger<WebhooksReceivedController> logger, IHooksRepository hooksRepository)
{
_settings = settings.Value;
_logger = logger;
_hooksRepository = hooksRepository;
}
[HttpPost]
public async Task<IActionResult> NewWebhook(WebhookData hook)
{
var header = Request.Headers[HeaderNames.WebHookCheckHeader];
var token = header.FirstOrDefault();
_logger.LogInformation($"Received hook with token {token}. My token is {_settings.Token}. Token validation is set to {_settings.ValidateToken}");
if (!_settings.ValidateToken || _settings.Token == token)
{
_logger.LogInformation($"Received hook is going to be processed");
var newHook = new WebHookReceived()
{
Data = hook.Payload,
When = hook.When,
Token = token
};
await _hooksRepository.AddNew(newHook);
_logger.LogInformation($"Received hook was processed.");
return Ok(newHook);
}
_logger.LogInformation($"Received hook is NOT processed - Bad Request returned.");
return BadRequest();
}
}
}

+ 20
- 0
src/Web/WebhookClient/Dockerfile View File

@ -0,0 +1,20 @@
FROM microsoft/dotnet:2.2-aspnetcore-runtime AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM microsoft/dotnet:2.2-sdk AS build
WORKDIR /src
COPY ["src/Web/WebhookClient/WebhookClient.csproj", "src/Web/WebhookClient/"]
RUN dotnet restore "src/Web/WebhookClient/WebhookClient.csproj"
COPY . .
WORKDIR "/src/src/Web/WebhookClient"
RUN dotnet build "WebhookClient.csproj" -c Release -o /app
FROM build AS publish
RUN dotnet publish "WebhookClient.csproj" -c Release -o /app
FROM base AS final
WORKDIR /app
COPY --from=publish /app .
ENTRYPOINT ["dotnet", "WebhookClient.dll"]

+ 12
- 0
src/Web/WebhookClient/HeaderNames.cs View File

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace WebhookClient
{
static class HeaderNames
{
public const string WebHookCheckHeader = "X-eshop-whtoken";
}
}

+ 51
- 0
src/Web/WebhookClient/HttpClientAuthorizationDelegatingHandler.cs View File

@ -0,0 +1,51 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
namespace WebhookClient
{
public class HttpClientAuthorizationDelegatingHandler
: DelegatingHandler
{
private readonly IHttpContextAccessor _httpContextAccesor;
public HttpClientAuthorizationDelegatingHandler(IHttpContextAccessor httpContextAccesor)
{
_httpContextAccesor = httpContextAccesor;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var authorizationHeader = _httpContextAccesor.HttpContext
.Request.Headers["Authorization"];
if (!string.IsNullOrEmpty(authorizationHeader))
{
request.Headers.Add("Authorization", new List<string>() { authorizationHeader });
}
var token = await GetToken();
if (token != null)
{
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
}
return await base.SendAsync(request, cancellationToken);
}
async Task<string> GetToken()
{
const string ACCESS_TOKEN = "access_token";
return await _httpContextAccesor.HttpContext
.GetTokenAsync(ACCESS_TOKEN);
}
}
}

+ 15
- 0
src/Web/WebhookClient/Models/WebHookReceived.cs View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace WebhookClient.Models
{
public class WebHookReceived
{
public DateTime When { get; set; }
public string Data { get; set; }
public string Token { get; set; }
}
}

+ 16
- 0
src/Web/WebhookClient/Models/WebhookData.cs View File

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace WebhookClient.Models
{
public class WebhookData
{
public DateTime When { get; set; }
public string Payload { get; set; }
public string Type { get; set; }
}
}

+ 14
- 0
src/Web/WebhookClient/Models/WebhookResponse.cs View File

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace WebhookClient.Models
{
public class WebhookResponse
{
public DateTime Date { get; set; }
public string DestUrl { get; set; }
public string Token { get; set; }
}
}

+ 15
- 0
src/Web/WebhookClient/Models/WebhookSubscriptionRequest.cs View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace WebhookClient.Models
{
public class WebhookSubscriptionRequest
{
public string Url { get; set; }
public string Token { get; set; }
public string Event { get; set; }
public string GrantUrl { get; set; }
}
}

+ 26
- 0
src/Web/WebhookClient/Pages/Error.cshtml View File

@ -0,0 +1,26 @@
@page
@model ErrorModel
@{
ViewData["Title"] = "Error";
}
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
@if (Model.ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@Model.RequestId</code>
</p>
}
<h3>Development Mode</h3>
<p>
Swapping to the <strong>Development</strong> environment displays detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>

+ 23
- 0
src/Web/WebhookClient/Pages/Error.cshtml.cs View File

@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace WebhookClient.Pages
{
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public class ErrorModel : PageModel
{
public string RequestId { get; set; }
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
public void OnGet()
{
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
}
}
}

+ 31
- 0
src/Web/WebhookClient/Pages/Index.cshtml View File

@ -0,0 +1,31 @@
@page
@model IndexModel
@{
ViewData["Title"] = "Home page";
}
<div class="text-center">
<h1 class="display-4">Welcome</h1>
<p>eShopOnContainers - Webhook client</p>
@if (!User.Identity.IsAuthenticated)
{
<a class="btn-primary btn" href="@Url.Action("SignIn", "Account")">Login</a>
<p>Why I need to login? You only need to login <bold>to setup a new webhook</bold>.</p>
}
</div>
<div class="table">
<h3>Current webhooks received</h3>
<p>(Data since last time web started up)<p>
<table class="table">
@foreach (var webhook in Model.WebHooksReceived)
{
<tr>
<td>@webhook.When</td>
<td><pre>@webhook.Data</pre></td>
<td>@(webhook.Token ?? "--None--")</td>
</tr>
}
</table>
</div>

+ 30
- 0
src/Web/WebhookClient/Pages/Index.cshtml.cs View File

@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Http;
using WebhookClient.Models;
using WebhookClient.Services;
namespace WebhookClient.Pages
{
public class IndexModel : PageModel
{
private readonly IHooksRepository _hooksRepository;
public IndexModel(IHooksRepository hooksRepository)
{
_hooksRepository = hooksRepository;
}
public IEnumerable<WebHookReceived> WebHooksReceived { get; private set; }
public async Task OnGet()
{
WebHooksReceived = await _hooksRepository.GetAll();
}
}
}

+ 8
- 0
src/Web/WebhookClient/Pages/Privacy.cshtml View File

@ -0,0 +1,8 @@
@page
@model PrivacyModel
@{
ViewData["Title"] = "Privacy Policy";
}
<h1>@ViewData["Title"]</h1>
<p>Use this page to detail your site's privacy policy.</p>

+ 16
- 0
src/Web/WebhookClient/Pages/Privacy.cshtml.cs View File

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace WebhookClient.Pages
{
public class PrivacyModel : PageModel
{
public void OnGet()
{
}
}
}

+ 19
- 0
src/Web/WebhookClient/Pages/RegisterWebhook.cshtml View File

@ -0,0 +1,19 @@
@page
@model WebhookClient.Pages.RegisterWebhookModel
@{
ViewData["Title"] = "RegisterWebhook";
}
<h3>Register webhook</h3>
<p>This page registers the "OrderPaid" Webhook by sending a POST to webhooks.</p>
<form method="post">
<p>Token: <input type="text" asp-for="Token" /></p>
<input type="submit" value="send" />
</form>
@if (Model.ResponseCode != (int)System.Net.HttpStatusCode.OK)
{
<p>Error @Model.ResponseCode (@Model.ResponseMessage) when calling the Webhooks API (@Model.RequestUrl) with GrantUrl: @Model.GrantUrl):(</p>
}

+ 81
- 0
src/Web/WebhookClient/Pages/RegisterWebhook.cshtml.cs View File

@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Formatting;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Options;
using WebhookClient.Models;
namespace WebhookClient.Pages
{
[Authorize]
public class RegisterWebhookModel : PageModel
{
private readonly Settings _settings;
private readonly IHttpClientFactory _httpClientFactory;
[BindProperty] public string Token { get; set; }
public int ResponseCode { get; set; }
public string RequestUrl { get; set; }
public string GrantUrl { get; set; }
public string ResponseMessage { get; set; }
public RegisterWebhookModel(IOptions<Settings> settings, IHttpClientFactory httpClientFactory)
{
_settings = settings.Value;
_httpClientFactory = httpClientFactory;
}
public void OnGet()
{
ResponseCode = (int)HttpStatusCode.OK;
Token = _settings.Token;
}
public async Task<IActionResult> OnPost()
{
ResponseCode = (int)HttpStatusCode.OK;
var protocol = Request.IsHttps ? "https" : "http";
var selfurl = !string.IsNullOrEmpty(_settings.SelfUrl) ? _settings.SelfUrl : $"{protocol}://{Request.Host}/{Request.PathBase}";
if (!selfurl.EndsWith("/"))
{
selfurl = selfurl + "/";
}
var granturl = $"{selfurl}check";
var url = $"{selfurl}webhook-received";
var client = _httpClientFactory.CreateClient("GrantClient");
var payload = new WebhookSubscriptionRequest()
{
Event = "OrderPaid",
GrantUrl = granturl,
Url = url,
Token = Token
};
var response = await client.PostAsync<WebhookSubscriptionRequest>(_settings.WebhooksUrl + "/api/v1/webhooks", payload, new JsonMediaTypeFormatter());
if (response.IsSuccessStatusCode)
{
return RedirectToPage("WebhooksList");
}
else
{
ResponseCode = (int)response.StatusCode;
ResponseMessage = response.ReasonPhrase;
GrantUrl = granturl;
RequestUrl = $"{response.RequestMessage.Method} {response.RequestMessage.RequestUri}";
}
return Page();
}
}
}

+ 79
- 0
src/Web/WebhookClient/Pages/Shared/_Layout.cshtml View File

@ -0,0 +1,79 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - WebhookClient</title>
<environment include="Development">
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
</environment>
<environment exclude="Development">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.1.3/css/bootstrap.min.css"
asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-value="absolute"
crossorigin="anonymous"
integrity="sha256-eSi1q2PG6J7g7ib17yAaWMcrr5GrtohYChqibrV7PBE="/>
</environment>
<link rel="stylesheet" href="~/css/site.css" />
</head>
<body>
<header>
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
<div class="container">
<a class="navbar-brand" asp-area="" asp-page="/Index">WebhookClient</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse" aria-controls="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse">
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/Index">Home</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/RegisterWebhook">Register webhook</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/WebhooksList">Webhooks registered (in API)</a>
</li>
</ul>
</div>
</div>
</nav>
</header>
<div class="container">
<main role="main" class="pb-3">
@RenderBody()
</main>
</div>
<footer class="border-top footer text-muted">
<div class="container">
&copy; 2019 - WebhookClient - <a asp-area="" asp-page="/RegisterWebhook">Register Webhook</a> | <a asp-area="" asp-page="/WebhooksList">Webhooks registered in API</a>
</div>
</footer>
<environment include="Development">
<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.js"></script>
</environment>
<environment exclude="Development">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"
asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
asp-fallback-test="window.jQuery"
crossorigin="anonymous"
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=">
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.1.3/js/bootstrap.bundle.min.js"
asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"
asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal"
crossorigin="anonymous"
integrity="sha256-E/V4cWE4qvAeO5MOhjtGtqDzPndRO1LBk8lJ/PR7CA4=">
</script>
</environment>
<script src="~/js/site.js" asp-append-version="true"></script>
@RenderSection("Scripts", required: false)
</body>
</html>

+ 18
- 0
src/Web/WebhookClient/Pages/Shared/_ValidationScriptsPartial.cshtml View File

@ -0,0 +1,18 @@
<environment include="Development">
<script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
</environment>
<environment exclude="Development">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.17.0/jquery.validate.min.js"
asp-fallback-src="~/lib/jquery-validation/dist/jquery.validate.min.js"
asp-fallback-test="window.jQuery && window.jQuery.validator"
crossorigin="anonymous"
integrity="sha256-F6h55Qw6sweK+t7SiOJX+2bpSAa3b/fnlrVCJvmEj1A=">
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validation-unobtrusive/3.2.11/jquery.validate.unobtrusive.min.js"
asp-fallback-src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"
asp-fallback-test="window.jQuery && window.jQuery.validator && window.jQuery.validator.unobtrusive"
crossorigin="anonymous"
integrity="sha256-9GycpJnliUjJDVDqP0UEu/bsm9U+3dnQUH8+3W10vkY=">
</script>
</environment>

+ 28
- 0
src/Web/WebhookClient/Pages/WebhooksList.cshtml View File

@ -0,0 +1,28 @@
@page
@model WebhookClient.Pages.WebhooksListModel
@{
ViewData["Title"] = "WebhooksList";
}
<h1>List of Webhooks registered by user @User.Identity.Name</h1>
<table class="table">
<thead class="thead-light">
<tr>
<th scope="col">Date</th>
<th scope="col">Destination Url</th>
<th scope="col">Validation token</th>
</tr>
</thead>
@foreach (var whr in Model.Webhooks)
{
<tr>
<td>@whr.Date</td>
<td>@whr.DestUrl</td>
<td>@whr.Token</td>
</tr>
}
</table>

+ 28
- 0
src/Web/WebhookClient/Pages/WebhooksList.cshtml.cs View File

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using WebhookClient.Models;
using WebhookClient.Services;
namespace WebhookClient.Pages
{
public class WebhooksListModel : PageModel
{
private readonly IWebhooksClient _webhooksClient;
public IEnumerable<WebhookResponse> Webhooks { get; private set; }
public WebhooksListModel(IWebhooksClient webhooksClient)
{
_webhooksClient = webhooksClient;
}
public async Task OnGet()
{
Webhooks = await _webhooksClient.LoadWebhooks();
}
}
}

+ 3
- 0
src/Web/WebhookClient/Pages/_ViewImports.cshtml View File

@ -0,0 +1,3 @@
@using WebhookClient
@namespace WebhookClient.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

+ 3
- 0
src/Web/WebhookClient/Pages/_ViewStart.cshtml View File

@ -0,0 +1,3 @@
@{
Layout = "_Layout";
}

+ 24
- 0
src/Web/WebhookClient/Program.cs View File

@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace WebhookClient
{
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}
}

+ 32
- 0
src/Web/WebhookClient/Properties/launchSettings.json View File

@ -0,0 +1,32 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:51921",
"sslPort": 44398
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"WebhookClient": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:5001;http://localhost:5000"
},
"Docker": {
"commandName": "Docker",
"launchBrowser": true,
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}"
}
}
}

+ 14
- 0
src/Web/WebhookClient/Services/IHooksRepository.cs View File

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using WebhookClient.Models;
namespace WebhookClient.Services
{
public interface IHooksRepository
{
Task<IEnumerable<WebHookReceived>> GetAll();
Task AddNew(WebHookReceived hook);
}
}

+ 13
- 0
src/Web/WebhookClient/Services/IWebhooksClient.cs View File

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using WebhookClient.Models;
namespace WebhookClient.Services
{
public interface IWebhooksClient
{
Task<IEnumerable<WebhookResponse>> LoadWebhooks();
}
}

+ 26
- 0
src/Web/WebhookClient/Services/InMemoryHooksRepository.cs View File

@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using WebhookClient.Models;
namespace WebhookClient.Services
{
public class InMemoryHooksRepository : IHooksRepository
{
private readonly List<WebHookReceived> _data;
public InMemoryHooksRepository() => _data = new List<WebHookReceived>();
public Task AddNew(WebHookReceived hook)
{
_data.Add(hook);
return Task.CompletedTask;
}
public Task<IEnumerable<WebHookReceived>> GetAll()
{
return Task.FromResult(_data.AsEnumerable());
}
}
}

+ 30
- 0
src/Web/WebhookClient/Services/WebhooksClient.cs View File

@ -0,0 +1,30 @@
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using WebhookClient.Models;
namespace WebhookClient.Services
{
public class WebhooksClient : IWebhooksClient
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly Settings _settings;
public WebhooksClient(IHttpClientFactory httpClientFactory, IOptions<Settings> settings)
{
_httpClientFactory = httpClientFactory;
_settings = settings.Value;
}
public async Task<IEnumerable<WebhookResponse>> LoadWebhooks()
{
var client = _httpClientFactory.CreateClient("GrantClient");
var response = await client.GetAsync(_settings.WebhooksUrl + "/api/v1/webhooks");
var json = await response.Content.ReadAsStringAsync();
var subscriptions = JsonConvert.DeserializeObject<IEnumerable<WebhookResponse>>(json);
return subscriptions;
}
}
}

+ 19
- 0
src/Web/WebhookClient/Settings.cs View File

@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace WebhookClient
{
public class Settings
{
public string Token { get; set; }
public string IdentityUrl { get; set; }
public string CallBackUrl { get; set; }
public string WebhooksUrl { get; set; }
public string SelfUrl { get; set; }
public bool ValidateToken { get; set; }
}
}

+ 151
- 0
src/Web/WebhookClient/Startup.cs View File

@ -0,0 +1,151 @@
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Linq;
using System.Net;
using System.Threading;
using WebhookClient.Services;
namespace WebhookClient
{
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 void ConfigureServices(IServiceCollection services)
{
services
.AddSession(opt =>
{
opt.Cookie.Name = ".eShopWebhooks.Session";
})
.AddConfiguration(Configuration)
.AddHttpClientServices(Configuration)
.AddCustomAuthentication(Configuration)
.AddTransient<IWebhooksClient, WebhooksClient>()
.AddSingleton<IHooksRepository, InMemoryHooksRepository>()
.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
var pathBase = Configuration["PATH_BASE"];
if (!string.IsNullOrEmpty(pathBase))
{
app.UsePathBase(pathBase);
}
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
}
app.UseAuthentication();
app.Map("/check", capp =>
{
capp.Run(async (context) =>
{
if ("OPTIONS".Equals(context.Request.Method, StringComparison.InvariantCultureIgnoreCase))
{
var validateToken = bool.TrueString.Equals(Configuration["ValidateToken"], StringComparison.InvariantCultureIgnoreCase);
var header = context.Request.Headers[HeaderNames.WebHookCheckHeader];
var value = header.FirstOrDefault();
var tokenToValidate = Configuration["Token"];
if (!validateToken || value == tokenToValidate)
{
if (!string.IsNullOrWhiteSpace(tokenToValidate))
{
context.Response.Headers.Add(HeaderNames.WebHookCheckHeader, tokenToValidate);
}
context.Response.StatusCode = (int)HttpStatusCode.OK;
}
else
{
await context.Response.WriteAsync("Invalid token");
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
}
}
else
{
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
}
});
});
app.UseStaticFiles();
app.UseSession();
app.UseMvcWithDefaultRoute();
}
}
static class ServiceExtensions
{
public static IServiceCollection AddConfiguration(this IServiceCollection services, IConfiguration configuration)
{
services.AddOptions();
services.Configure<Settings>(configuration);
return services;
}
public static IServiceCollection AddCustomAuthentication(this IServiceCollection services, IConfiguration configuration)
{
var identityUrl = configuration.GetValue<string>("IdentityUrl");
var callBackUrl = configuration.GetValue<string>("CallBackUrl");
// Add Authentication services
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(setup => setup.ExpireTimeSpan = TimeSpan.FromHours(2))
.AddOpenIdConnect(options =>
{
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.Authority = identityUrl.ToString();
options.SignedOutRedirectUri = callBackUrl.ToString();
options.ClientId = "webhooksclient";
options.ClientSecret = "secret";
options.ResponseType = "code id_token";
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.RequireHttpsMetadata = false;
options.Scope.Add("openid");
options.Scope.Add("webhooks");
});
return services;
}
public static IServiceCollection AddHttpClientServices(this IServiceCollection services, IConfiguration configuration)
{
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddTransient<HttpClientAuthorizationDelegatingHandler>();
services.AddHttpClient("extendedhandlerlifetime").SetHandlerLifetime(Timeout.InfiniteTimeSpan);
//add http client services
services.AddHttpClient("GrantClient")
.SetHandlerLifetime(TimeSpan.FromMinutes(5))
.AddHttpMessageHandler<HttpClientAuthorizationDelegatingHandler>();
return services;
}
}
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save