@ -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,29 @@ | |||||
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; | |||||
namespace Webhooks.API.IntegrationEvents | |||||
{ | |||||
public class OrderStatusChangedToShippedIntegrationEventHandler : IIntegrationEventHandler<OrderStatusChangedToShippedIntegrationEvent> | |||||
{ | |||||
private readonly IWebhooksRetriever _retriever; | |||||
private readonly IWebhooksSender _sender; | |||||
public OrderStatusChangedToShippedIntegrationEventHandler(IWebhooksRetriever retriever, IWebhooksSender sender ) | |||||
{ | |||||
_retriever = retriever; | |||||
_sender = sender; | |||||
} | |||||
public async Task Handle(OrderStatusChangedToShippedIntegrationEvent @event) | |||||
{ | |||||
var subscriptions = await _retriever.GetSubscriptionsOfType(WebhookType.OrderShipped); | |||||
var whook = new WebhookData(WebhookType.OrderShipped, @event); | |||||
await _sender.SendAll(subscriptions, whook); | |||||
} | |||||
} | |||||
} |
@ -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,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,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,24 @@ | |||||
using Newtonsoft.Json; | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Threading.Tasks; | |||||
namespace Microsoft.AspNetCore.Http | |||||
{ | |||||
static class ISessionExtensions | |||||
{ | |||||
public static void Set<T>(this ISession session, string key, T value) | |||||
{ | |||||
session.SetString(key, JsonConvert.SerializeObject(value)); | |||||
} | |||||
public static T Get<T>(this ISession session, string key) | |||||
{ | |||||
var value = session.GetString(key); | |||||
return value == null ? default(T) : | |||||
JsonConvert.DeserializeObject<T>(value); | |||||
} | |||||
} | |||||
} |
@ -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,13 @@ | |||||
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; } | |||||
} | |||||
} |
@ -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,14 @@ | |||||
@page | |||||
@model WebhookClient.Pages.RegisterWebhookModel | |||||
@{ | |||||
ViewData["Title"] = "RegisterWebhook"; | |||||
} | |||||
<h3>Register webhook</h3> | |||||
<p>This page registers the "OrderShipped" 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> |
@ -0,0 +1,55 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Net.Http; | |||||
using System.Net.Http.Formatting; | |||||
using System.Threading.Tasks; | |||||
using Microsoft.AspNetCore.Hosting; | |||||
using Microsoft.AspNetCore.Mvc; | |||||
using Microsoft.AspNetCore.Mvc.RazorPages; | |||||
using Microsoft.Extensions.Options; | |||||
using WebhookClient.Models; | |||||
namespace WebhookClient.Pages | |||||
{ | |||||
public class RegisterWebhookModel : PageModel | |||||
{ | |||||
private readonly Settings _settings; | |||||
private readonly IHttpClientFactory _httpClientFactory; | |||||
[BindProperty] public string Token { get; set; } | |||||
public RegisterWebhookModel(IOptions<Settings> settings, IHttpClientFactory httpClientFactory) | |||||
{ | |||||
_settings = settings.Value; | |||||
_httpClientFactory = httpClientFactory; | |||||
} | |||||
public void OnGet() | |||||
{ | |||||
Token = _settings.Token; | |||||
} | |||||
public async Task OnPost() | |||||
{ | |||||
var protocol = Request.IsHttps ? "https" : "http"; | |||||
var selfurl = !string.IsNullOrEmpty(_settings.SelfUrl) ? _settings.SelfUrl : $"{protocol}://{Request.Host}/{Request.PathBase}"; | |||||
var granturl = $"{selfurl}check"; | |||||
var url = $"{selfurl}webhook"; | |||||
var client = _httpClientFactory.CreateClient("GrantClient"); | |||||
var payload = new WebhookSubscriptionRequest() | |||||
{ | |||||
Event = "OrderShipped", | |||||
GrantUrl = granturl, | |||||
Url = url, | |||||
Token = Token | |||||
}; | |||||
var response = await client.PostAsync<WebhookSubscriptionRequest>(_settings.WebhooksUrl + "/api/v1/webhooks", payload, new JsonMediaTypeFormatter()); | |||||
RedirectToPage("Index"); | |||||
} | |||||
} | |||||
} |
@ -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,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,29 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Threading.Tasks; | |||||
using WebhookClient.Models; | |||||
namespace WebhookClient.Services | |||||
{ | |||||
public class WebhooksClient : IWebhooksClient | |||||
{ | |||||
public async Task<IEnumerable<WebhookResponse>> LoadWebhooks() | |||||
{ | |||||
return new[]{ | |||||
new WebhookResponse() | |||||
{ | |||||
Date = DateTime.Now, | |||||
DestUrl = "http://aaaaa.me", | |||||
Token = "3282832as2" | |||||
}, | |||||
new WebhookResponse() | |||||
{ | |||||
Date = DateTime.Now.Subtract(TimeSpan.FromSeconds(392)), | |||||
DestUrl = "http://bbbbb.me", | |||||
Token = "ds2" | |||||
} | |||||
}; | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,17 @@ | |||||
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; } | |||||
} | |||||
} |