@ -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; | |||
} | |||
} | |||
} |