@ -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 |
@ -0,0 +1,5 @@ | |||||
apiVersion: v1 | |||||
appVersion: "1.0" | |||||
description: A Helm chart for Kubernetes | |||||
name: webhooks-api | |||||
version: 0.1.0 |
@ -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 |
@ -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 -}} |
@ -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 -}} |
@ -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 }}" |
@ -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 }} | |||||
@ -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 }} |
@ -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 }} |
@ -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' | |||||
@ -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 |
@ -0,0 +1,5 @@ | |||||
apiVersion: v1 | |||||
appVersion: "1.0" | |||||
description: A Helm chart for Kubernetes | |||||
name: webhooks-web | |||||
version: 0.1.0 |
@ -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 |
@ -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 -}} |
@ -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 -}} |
@ -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 }} |
@ -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 }} | |||||
@ -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 }} |
@ -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 }} |
@ -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 | |||||
@ -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"); | |||||
} | |||||
} | |||||
} |
@ -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) }); | |||||
} | |||||
} | |||||
} | |||||
} |
@ -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"); | |||||
} | |||||
} | |||||
} |
@ -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"] |
@ -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 | |||||
{ | |||||
} | |||||
} |
@ -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; | |||||
} | |||||
} | |||||
} |
@ -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" } } | |||||
}); | |||||
} | |||||
} | |||||
} | |||||
} |
@ -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; } | |||||
} | |||||
} | |||||
} |
@ -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); | |||||
} | |||||
} | |||||
} |
@ -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; | |||||
} | |||||
} | |||||
} |
@ -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); | |||||
} | |||||
} | |||||
} |
@ -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; | |||||
} | |||||
} | |||||
} |
@ -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); | |||||
} | |||||
} | |||||
} |
@ -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; | |||||
} | |||||
} | |||||
} |
@ -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; | |||||
} | |||||
} | |||||
} |
@ -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 | |||||
} | |||||
} | |||||
} |
@ -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"); | |||||
} | |||||
} | |||||
} |
@ -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 | |||||
} | |||||
} | |||||
} |
@ -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); | |||||
} | |||||
} | |||||
} |
@ -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; } | |||||
} | |||||
} |
@ -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 | |||||
} | |||||
} |
@ -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>(); | |||||
} | |||||
} |
@ -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}" | |||||
} | |||||
} | |||||
} |
@ -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; | |||||
} | |||||
} | |||||
} |
@ -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); | |||||
} | |||||
} |
@ -0,0 +1,7 @@ | |||||
namespace Webhooks.API.Services | |||||
{ | |||||
public interface IIdentityService | |||||
{ | |||||
string GetUserIdentity(); | |||||
} | |||||
} |
@ -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); | |||||
} | |||||
} |
@ -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); | |||||
} | |||||
} |
@ -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; | |||||
} | |||||
} | |||||
} |
@ -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; | |||||
} | |||||
} | |||||
} |
@ -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); | |||||
} | |||||
} | |||||
} |
@ -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; | |||||
} | |||||
} | |||||
} |
@ -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> |
@ -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" | |||||
} |
@ -0,0 +1,10 @@ | |||||
{ | |||||
"Logging": { | |||||
"LogLevel": { | |||||
"Default": "Warning" | |||||
} | |||||
}, | |||||
"AllowedHosts": "*", | |||||
"SubscriptionClientName": "Webhooks", | |||||
"EventBusRetryCount": 5 | |||||
} |
@ -0,0 +1 @@ | |||||
!wwwroot |
@ -0,0 +1 @@ | |||||
!wwwroot/lib |
@ -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 }); | |||||
} | |||||
} | |||||
} |
@ -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(); | |||||
} | |||||
} | |||||
} |
@ -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"] |
@ -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"; | |||||
} | |||||
} |
@ -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); | |||||
} | |||||
} | |||||
} |
@ -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; } | |||||
} | |||||
} |
@ -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; } | |||||
} | |||||
} |
@ -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; } | |||||
} | |||||
} |
@ -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; } | |||||
} | |||||
} |
@ -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> |
@ -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; | |||||
} | |||||
} | |||||
} |
@ -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> | |||||
@ -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(); | |||||
} | |||||
} | |||||
} |
@ -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> |
@ -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() | |||||
{ | |||||
} | |||||
} | |||||
} |
@ -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> | |||||
} |
@ -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(); | |||||
} | |||||
} | |||||
} |
@ -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"> | |||||
© 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> |
@ -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> |
@ -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> | |||||
@ -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(); | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,3 @@ | |||||
@using WebhookClient | |||||
@namespace WebhookClient.Pages | |||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers |
@ -0,0 +1,3 @@ | |||||
@{ | |||||
Layout = "_Layout"; | |||||
} |
@ -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>(); | |||||
} | |||||
} |
@ -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}" | |||||
} | |||||
} | |||||
} |
@ -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); | |||||
} | |||||
} |
@ -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(); | |||||
} | |||||
} |
@ -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()); | |||||
} | |||||
} | |||||
} |
@ -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; | |||||
} | |||||
} | |||||
} |
@ -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; } | |||||
} | |||||
} |
@ -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; | |||||
} | |||||
} | |||||
} |