Implemented Mailtrap Webhook API
Some checks failed
Build, Tag, Push Docker Image / build (push) Has been cancelled
Create Release / Generate Release (push) Has been cancelled

This commit is contained in:
Mia Rose Winter 2024-03-12 14:23:00 +01:00
parent a530375669
commit f27ba8200f
Signed by: miawinter
GPG key ID: 4B6F6A83178F595E
6 changed files with 123 additions and 2 deletions

2
Wave.sln.DotSettings Normal file
View file

@ -0,0 +1,2 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/UserDictionary/Words/=mailtrap/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View file

@ -3,6 +3,7 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using AspNetCore.Authentication.ApiKey;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.OutputCaching; using Microsoft.AspNetCore.OutputCaching;
using Wave.Data; using Wave.Data;
@ -33,7 +34,7 @@ public class ApiController(ApplicationDbContext context, IOptions<Customization>
[HttpGet("email/subscriber/{email}")] [HttpGet("email/subscriber/{email}")]
[Produces("application/json")] [Produces("application/json")]
[Authorize("EmailApi")] [Authorize("EmailApi", AuthenticationSchemes = ApiKeyDefaults.AuthenticationScheme)]
[OutputCache(Duration = 60*10)] [OutputCache(Duration = 60*10)]
public async Task<Results<Ok<EmailSubscriberDto>, NotFound>> GetEmailSubscriber([EmailAddress] string email) { public async Task<Results<Ok<EmailSubscriberDto>, NotFound>> GetEmailSubscriber([EmailAddress] string email) {
var subscriber = await context.Set<EmailSubscriber>() var subscriber = await context.Set<EmailSubscriber>()

View file

@ -0,0 +1,63 @@
using Humanizer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Wave.Data;
using Wave.Data.Api.Mailtrap;
namespace Wave.Controllers;
[ApiController]
[Route("/api/[controller]")]
public class WebhookController(ILogger<WebhookController> logger, ApplicationDbContext context) : ControllerBase {
[HttpPost("mailtrap/{apiKey}")]
[Authorize("EmailApi", AuthenticationSchemes = "ApiKeyInRoute")]
public async Task<IActionResult> Mailtrap(Webhook webhook, string apiKey) {
Console.WriteLine(apiKey);
foreach (var webhookEvent in webhook.Events) {
var subscriber = await context.Set<EmailSubscriber>().FirstOrDefaultAsync(s => s.Email == webhookEvent.Email);
if (subscriber is null) {
logger.LogWarning(
"Received webhook event from mailtrap of type {type}, but failed to find subscriber with E-Mail {email}.",
webhookEvent.Type, webhookEvent.Email);
continue;
}
// ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault
switch (webhookEvent.Type) {
case WebhookEventType.Delivery:
subscriber.LastMailReceived = webhookEvent.EventDateTime;
break;
case WebhookEventType.Open:
subscriber.LastMailOpened = webhookEvent.EventDateTime;
break;
case WebhookEventType.Bounce:
// Store this message in case it develops into a suspension
subscriber.UnsubscribeReason = webhookEvent.Response;
break;
case WebhookEventType.Suspension:
subscriber.Unsubscribed = true;
subscriber.UnsubscribeReason ??= "unknown";
break;
case WebhookEventType.Unsubscribe:
subscriber.Unsubscribed = true;
subscriber.UnsubscribeReason ??= "User Unsubscribed";
break;
case WebhookEventType.SpamComplaint:
subscriber.Unsubscribed = true;
subscriber.UnsubscribeReason ??= "User reported as Spam";
break;
case WebhookEventType.Reject:
subscriber.Unsubscribed = true;
subscriber.UnsubscribeReason ??= webhookEvent.Reason?.Humanize().Titleize() ?? "Rejected";
break;
}
await context.SaveChangesAsync();
}
return Ok();
}
}

View file

@ -0,0 +1,49 @@
using System.Text.Json.Serialization;
namespace Wave.Data.Api.Mailtrap;
public enum WebhookEventType {
Delivery,
SoftBounce,
Bounce,
Suspension,
Unsubscribe,
Open,
SpamComplaint,
Click,
Reject
}
public record WebhookEvent(
[property:JsonPropertyName("event")]
string EventTypeString,
[property:JsonPropertyName("category")]
string Category,
[property:JsonPropertyName("message_id")]
string MessageId,
[property:JsonPropertyName("event_id")]
string EventId,
[property:JsonPropertyName("email")]
string Email,
[property:JsonPropertyName("timestamp")]
long Timestamp,
[property:JsonPropertyName("response")]
string? Response,
[property:JsonPropertyName("reason")]
string? Reason,
[property:JsonPropertyName("response_code")]
int? ResponseCode) {
public WebhookEventType Type => Enum.Parse<WebhookEventType>(EventTypeString.Replace("_", ""), true);
public DateTimeOffset EventDateTime => DateTimeOffset.FromUnixTimeMilliseconds(Timestamp);
}
public record Webhook {
[JsonPropertyName("events")]
[JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)]
public ICollection<WebhookEvent> Events { get; } = [];
public override string ToString() {
return $"Webhook {{ Events = [{string.Join(", ", Events)}] }}";
}
}

View file

@ -101,6 +101,9 @@
}).AddApiKeyInHeader<ApiKeyProvider>(ApiKeyDefaults.AuthenticationScheme, options => { }).AddApiKeyInHeader<ApiKeyProvider>(ApiKeyDefaults.AuthenticationScheme, options => {
options.KeyName = "X-API-KEY"; options.KeyName = "X-API-KEY";
options.Realm = "Wave API"; options.Realm = "Wave API";
}).AddApiKeyInRouteValues<ApiKeyProvider>("ApiKeyInRoute", options => {
options.KeyName = "apiKey";
options.Realm = "Wave API";
}) })
.AddIdentityCookies(); .AddIdentityCookies();
if (builder.Configuration.GetSection("Oidc").Get<OidcConfiguration>() is {} oidc && !string.IsNullOrWhiteSpace(oidc.Authority)) { if (builder.Configuration.GetSection("Oidc").Get<OidcConfiguration>() is {} oidc && !string.IsNullOrWhiteSpace(oidc.Authority)) {

View file

@ -13,7 +13,10 @@ private record ActualApiKey(string Key, string OwnerName, IReadOnlyCollection<Cl
public async Task<IApiKey?> ProvideAsync(string key) { public async Task<IApiKey?> ProvideAsync(string key) {
try { try {
byte[] data = Convert.FromBase64String(key); string unescapedKey = key;
if (unescapedKey.Contains('%')) unescapedKey = Uri.UnescapeDataString(key);
byte[] data = Convert.FromBase64String(unescapedKey);
string hashedKey = Convert.ToBase64String(SHA256.HashData(data)); string hashedKey = Convert.ToBase64String(SHA256.HashData(data));
var apiKey = await context.Set<ApiKey>().Include(a => a.ApiClaims).SingleOrDefaultAsync(k => k.Key == hashedKey); var apiKey = await context.Set<ApiKey>().Include(a => a.ApiClaims).SingleOrDefaultAsync(k => k.Key == hashedKey);