V14: Webhook Management API (#15147)

* Add webhook to management api

* Update webhook controllers

* Add ByKey webhook controller

* Fix typo

* Fix typo

* Update multiple webhooks

* Update using

* Remove duplicate constant after merge

* Fix typo in file name

* Update casing of IWebhookService

* Fix typo

* Use Webhook entity type

* Fix ambiguous reference

* Update webhook mapping

* Update after change of CreatedAtAction

* Use CreatedAtId instead

* Update src/Umbraco.Cms.Api.Management/Controllers/Webhook/ByKeyWebhookController.cs

Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>

* Update src/Umbraco.Cms.Api.Management/Controllers/Webhook/ByKeyWebhookController.cs

Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>

* Update src/Umbraco.Cms.Api.Management/ViewModels/Webhook/CreateWebhookRequestModel.cs

Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>

* Update src/Umbraco.Cms.Api.Management/Controllers/Webhook/DeleteWebhookController.cs

Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>

* Update src/Umbraco.Cms.Api.Management/Controllers/Webhook/CreateWebhookController.cs

Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>

* Add Guid to WebhookResponseModel

* Cleanup

* Add Auth

* Move webhook logic from backoffice to management api

* Add mapping

---------

Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>
This commit is contained in:
Bjarne Fyrstenborg
2024-02-26 14:35:35 +01:00
committed by GitHub
parent 0f6705795a
commit f47830b165
30 changed files with 459 additions and 217 deletions

View File

@@ -0,0 +1,40 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.Webhook;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Management.Controllers.Webhook;
[ApiVersion("1.0")]
public class ByKeyWebhookController : WebhookControllerBase
{
private readonly IWebhookService _webhookService;
private readonly IWebhookPresentationFactory _webhookPresentationFactory;
public ByKeyWebhookController(IWebhookService webhookService, IWebhookPresentationFactory webhookPresentationFactory)
{
_webhookService = webhookService;
_webhookPresentationFactory = webhookPresentationFactory;
}
[HttpGet("{id:guid}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(WebhookResponseModel), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> ByKey(Guid id)
{
IWebhook? webhook = await _webhookService.GetAsync(id);
if (webhook is null)
{
return WebhookOperationStatusResult(WebhookOperationStatus.NotFound);
}
WebhookResponseModel model = _webhookPresentationFactory.CreateResponseModel(webhook);
return Ok(model);
}
}

View File

@@ -0,0 +1,44 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.Webhook;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Web.Common.Authorization;
namespace Umbraco.Cms.Api.Management.Controllers.Webhook;
[ApiVersion("1.0")]
[Authorize(Policy = "New" + AuthorizationPolicies.TreeAccessWebhooks)]
public class CreateWebhookController : WebhookControllerBase
{
private readonly IWebhookService _webhookService;
private readonly IUmbracoMapper _umbracoMapper;
public CreateWebhookController(
IWebhookService webhookService, IUmbracoMapper umbracoMapper)
{
_webhookService = webhookService;
_umbracoMapper = umbracoMapper;
}
[HttpPost]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> Create(CreateWebhookRequestModel createWebhookRequestModel)
{
IWebhook created = _umbracoMapper.Map<IWebhook>(createWebhookRequestModel)!;
Attempt<IWebhook, WebhookOperationStatus> result = await _webhookService.CreateAsync(created);
return result.Success
? CreatedAtId<ByKeyWebhookController>(controller => nameof(controller.ByKey), result.Result!.Key)
: WebhookOperationStatusResult(result.Status);
}
}

View File

@@ -0,0 +1,40 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Web.Common.Authorization;
namespace Umbraco.Cms.Api.Management.Controllers.Webhook;
[ApiVersion("1.0")]
[Authorize(Policy = "New" + AuthorizationPolicies.TreeAccessWebhooks)]
public class DeleteWebhookController : WebhookControllerBase
{
private readonly IWebhookService _webhookService;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
public DeleteWebhookController(IWebhookService webhookService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
_webhookService = webhookService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
}
[HttpDelete($"{{{nameof(id)}}}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<IActionResult> Delete(Guid id)
{
Attempt<IWebhook?, WebhookOperationStatus> result = await _webhookService.DeleteAsync(id);
return result.Success
? Ok()
: WebhookOperationStatusResult(result.Status);
}
}

View File

@@ -0,0 +1,32 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.Webhook.Item;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.Webhook.Item;
[ApiVersion("1.0")]
public class ItemsWebhookEntityController : WebhookEntityControllerBase
{
private readonly IWebhookService _webhookService;
private readonly IUmbracoMapper _mapper;
public ItemsWebhookEntityController(IWebhookService webhookService, IUmbracoMapper mapper)
{
_webhookService = webhookService;
_mapper = mapper;
}
[HttpGet("item")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(IEnumerable<WebhookItemResponseModel>), StatusCodes.Status200OK)]
public async Task<ActionResult> Items([FromQuery(Name = "ids")] HashSet<Guid> ids)
{
IEnumerable<IWebhook?> webhooks = await _webhookService.GetMultipleAsync(ids);
List<WebhookItemResponseModel> entityResponseModels = _mapper.MapEnumerable<IWebhook?, WebhookItemResponseModel>(webhooks);
return Ok(entityResponseModels);
}
}

View File

@@ -0,0 +1,15 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Core;
using Umbraco.Cms.Web.Common.Authorization;
namespace Umbraco.Cms.Api.Management.Controllers.Webhook.Item;
[ApiController]
[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.Webhook}")]
[ApiExplorerSettings(GroupName = "Webhook")]
[Authorize(Policy = "New" + AuthorizationPolicies.TreeAccessWebhooks)]
public class WebhookEntityControllerBase : ManagementApiControllerBase
{
}

View File

@@ -0,0 +1,53 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.Webhook;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Core.Webhooks;
using Umbraco.Cms.Web.Common.Authorization;
namespace Umbraco.Cms.Api.Management.Controllers.Webhook;
[ApiVersion("1.0")]
[Authorize(Policy = "New" + AuthorizationPolicies.TreeAccessWebhooks)]
public class UpdateWebhookController : WebhookControllerBase
{
private readonly IWebhookService _webhookService;
private readonly IUmbracoMapper _umbracoMapper;
public UpdateWebhookController(
IWebhookService webhookService,
IUmbracoMapper umbracoMapper)
{
_webhookService = webhookService;
_umbracoMapper = umbracoMapper;
}
[HttpPut("{id:guid}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<IActionResult> Update(Guid id, UpdateWebhookRequestModel updateWebhookRequestModel)
{
IWebhook? current = await _webhookService.GetAsync(id);
if (current is null)
{
return WebhookNotFound();
}
IWebhook updated = _umbracoMapper.Map(updateWebhookRequestModel, current);
Attempt<IWebhook, WebhookOperationStatus> result = await _webhookService.UpdateAsync(updated);
return result.Success
? Ok()
: WebhookOperationStatusResult(result.Status);
}
}

View File

@@ -0,0 +1,30 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.Builders;
using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Management.Controllers.Webhook;
[ApiController]
[VersionedApiBackOfficeRoute("webhook")]
[ApiExplorerSettings(GroupName = "Webhook")]
public abstract class WebhookControllerBase : ManagementApiControllerBase
{
protected IActionResult WebhookOperationStatusResult(WebhookOperationStatus status) =>
status switch
{
WebhookOperationStatus.NotFound => WebhookNotFound(),
WebhookOperationStatus.CancelledByNotification => BadRequest(new ProblemDetailsBuilder()
.WithTitle("Cancelled by notification")
.WithDetail("A notification handler prevented the webhook operation.")
.Build()),
_ => StatusCode(StatusCodes.Status500InternalServerError, new ProblemDetailsBuilder()
.WithTitle("Unknown webhook operation status.")
.Build()),
};
protected IActionResult WebhookNotFound() => NotFound(new ProblemDetailsBuilder()
.WithTitle("The webhook could not be found")
.Build());
}

View File

@@ -98,6 +98,7 @@ internal static class BackOfficeAuthPolicyBuilderExtensions
AddPolicy(AuthorizationPolicies.TreeAccessScripts, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Settings);
AddPolicy(AuthorizationPolicies.TreeAccessStylesheets, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Settings);
AddPolicy(AuthorizationPolicies.TreeAccessTemplates, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Settings);
AddPolicy(AuthorizationPolicies.TreeAccessWebhooks, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Settings);
// Contextual permissions
// TODO: Rename policies once we have the old ones removed

View File

@@ -52,6 +52,7 @@ public static class UmbracoBuilderExtensions
.AddScripts()
.AddPartialViews()
.AddStylesheets()
.AddWebhooks()
.AddServer()
.AddCorsPolicy()
.AddBackOfficeAuthentication()

View File

@@ -0,0 +1,18 @@
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Web.BackOffice.Mapping;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.DependencyInjection;
internal static class WebhookBuilderExtensions
{
internal static IUmbracoBuilder AddWebhooks(this IUmbracoBuilder builder)
{
builder.WithCollectionBuilder<MapDefinitionCollectionBuilder>().Add<WebhookMapDefinition>();
builder.Services.AddUnique<IWebhookPresentationFactory, WebhookPresentationFactory>();
return builder;
}
}

View File

@@ -0,0 +1,13 @@
using Umbraco.Cms.Api.Management.ViewModels.Webhook;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Api.Management.Factories;
public interface IWebhookPresentationFactory
{
WebhookResponseModel CreateResponseModel(IWebhook webhook);
IWebhook CreateWebhook(CreateWebhookRequestModel webhookRequestModel);
IWebhook CreateWebhook(UpdateWebhookRequestModel webhookRequestModel);
}

View File

@@ -1,9 +1,9 @@
using Umbraco.Cms.Api.Management.ViewModels.Webhook;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Webhooks;
using Umbraco.Cms.Web.Common.Models;
namespace Umbraco.Cms.Web.BackOffice.Services;
namespace Umbraco.Cms.Api.Management.Factories;
internal class WebhookPresentationFactory : IWebhookPresentationFactory
{
@@ -11,27 +11,38 @@ internal class WebhookPresentationFactory : IWebhookPresentationFactory
public WebhookPresentationFactory(WebhookEventCollection webhookEventCollection) => _webhookEventCollection = webhookEventCollection;
public WebhookViewModel Create(IWebhook webhook)
public WebhookResponseModel CreateResponseModel(IWebhook webhook)
{
var target = new WebhookViewModel
var target = new WebhookResponseModel
{
ContentTypeKeys = webhook.ContentTypeKeys,
Events = webhook.Events.Select(Create).ToArray(),
Url = webhook.Url,
Enabled = webhook.Enabled,
Id = webhook.Id,
Key = webhook.Key,
Id = webhook.Key,
Headers = webhook.Headers,
ContentTypeKeys = webhook.ContentTypeKeys,
};
return target;
}
private WebhookEventViewModel Create(string alias)
public IWebhook CreateWebhook(CreateWebhookRequestModel webhookRequestModel)
{
var target = new Webhook(webhookRequestModel.Url, webhookRequestModel.Enabled, webhookRequestModel.ContentTypeKeys, webhookRequestModel.Events, webhookRequestModel.Headers);
return target;
}
public IWebhook CreateWebhook(UpdateWebhookRequestModel webhookRequestModel)
{
var target = new Webhook(webhookRequestModel.Url, webhookRequestModel.Enabled, webhookRequestModel.ContentTypeKeys, webhookRequestModel.Events, webhookRequestModel.Headers);
return target;
}
private WebhookEventResponseModel Create(string alias)
{
IWebhookEvent? webhookEvent = _webhookEventCollection.FirstOrDefault(x => x.Alias == alias);
return new WebhookEventViewModel
return new WebhookEventResponseModel
{
EventName = webhookEvent?.EventName ?? alias,
EventType = webhookEvent?.EventType ?? Constants.WebhookEvents.Types.Other,

View File

@@ -1,4 +1,4 @@
using Umbraco.Cms.Api.Management.ViewModels.DataType.Item;
using Umbraco.Cms.Api.Management.ViewModels.DataType.Item;
using Umbraco.Cms.Api.Management.ViewModels.Dictionary.Item;
using Umbraco.Cms.Api.Management.ViewModels.DocumentType.Item;
using Umbraco.Cms.Api.Management.ViewModels.Language.Item;
@@ -10,6 +10,7 @@ using Umbraco.Cms.Api.Management.ViewModels.RelationType.Item;
using Umbraco.Cms.Api.Management.ViewModels.Template.Item;
using Umbraco.Cms.Api.Management.ViewModels.User.Item;
using Umbraco.Cms.Api.Management.ViewModels.UserGroup.Item;
using Umbraco.Cms.Api.Management.ViewModels.Webhook.Item;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
@@ -33,6 +34,7 @@ public class ItemTypeMapDefinition : IMapDefinition
mapper.Define<IRelationType, RelationTypeItemResponseModel>((_, _) => new RelationTypeItemResponseModel(), Map);
mapper.Define<IUser, UserItemResponseModel>((_, _) => new UserItemResponseModel(), Map);
mapper.Define<IUserGroup, UserGroupItemResponseModel>((_, _) => new UserGroupItemResponseModel(), Map);
mapper.Define<IWebhook, WebhookItemResponseModel>((_, _) => new WebhookItemResponseModel(), Map);
}
// Umbraco.Code.MapAll
@@ -119,4 +121,14 @@ public class ItemTypeMapDefinition : IMapDefinition
target.Name = source.Name ?? source.Alias;
target.Icon = source.Icon;
}
// Umbraco.Code.MapAll
private static void Map(IWebhook source, WebhookItemResponseModel target, MapperContext context)
{
target.Name = string.Empty; //source.Name;
target.Url = source.Url;
target.Enabled = source.Enabled;
target.Events = string.Join(",", source.Events);
target.Types = string.Join(",", source.ContentTypeKeys);
}
}

View File

@@ -1,5 +1,4 @@
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Api.Management.ViewModels.Webhook;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
@@ -14,15 +13,6 @@ public class WebhookMapDefinition : IMapDefinition
private readonly IHostingEnvironment _hostingEnvironment;
private readonly ILocalizedTextService _localizedTextService;
[Obsolete("Use non-obsolete constructor. This will be removed in Umbraco 15.")]
public WebhookMapDefinition() : this(
StaticServiceProvider.Instance.GetRequiredService<IHostingEnvironment>(),
StaticServiceProvider.Instance.GetRequiredService<ILocalizedTextService>()
)
{
}
public WebhookMapDefinition(IHostingEnvironment hostingEnvironment, ILocalizedTextService localizedTextService)
{
_hostingEnvironment = hostingEnvironment;
@@ -31,31 +21,41 @@ public class WebhookMapDefinition : IMapDefinition
public void DefineMaps(IUmbracoMapper mapper)
{
mapper.Define<WebhookViewModel, IWebhook>((_, _) => new Webhook(string.Empty), Map);
mapper.Define<IWebhookEvent, WebhookEventViewModel>((_, _) => new WebhookEventViewModel(), Map);
mapper.Define<IWebhookEvent, WebhookEventResponseModel>((_, _) => new WebhookEventResponseModel(), Map);
mapper.Define<WebhookLog, WebhookLogViewModel>((_, _) => new WebhookLogViewModel(), Map);
}
// Umbraco.Code.MapAll -CreateDate -DeleteDate -Id -Key -UpdateDate
private void Map(WebhookViewModel source, IWebhook target, MapperContext context)
{
target.ContentTypeKeys = source.ContentTypeKeys;
target.Events = source.Events.Select(x => x.Alias).ToArray();
target.Url = source.Url;
target.Enabled = source.Enabled;
target.Id = source.Id;
target.Key = source.Key ?? Guid.NewGuid();
target.Headers = source.Headers;
mapper.Define<CreateWebhookRequestModel, IWebhook>((_, _) => new Webhook(string.Empty), Map);
mapper.Define<UpdateWebhookRequestModel, IWebhook>((_, _) => new Webhook(string.Empty), Map);
}
// Umbraco.Code.MapAll
private void Map(IWebhookEvent source, WebhookEventViewModel target, MapperContext context)
private void Map(IWebhookEvent source, WebhookEventResponseModel target, MapperContext context)
{
target.EventName = source.EventName;
target.EventType = source.EventType;
target.Alias = source.Alias;
}
// Umbraco.Code.MapAll -CreateDate -DeleteDate -Id -UpdateDate
private void Map(CreateWebhookRequestModel source, IWebhook target, MapperContext context)
{
target.Url = source.Url;
target.Enabled = source.Enabled;
target.ContentTypeKeys = source.ContentTypeKeys;
target.Events = source.Events;
target.Headers = source.Headers;
target.Key = source.Id ?? Guid.NewGuid();
}
// Umbraco.Code.MapAll -CreateDate -DeleteDate -Id -UpdateDate -Key
private void Map(UpdateWebhookRequestModel source, IWebhook target, MapperContext context)
{
target.Url = source.Url;
target.Enabled = source.Enabled;
target.ContentTypeKeys = source.ContentTypeKeys;
target.Events = source.Events;
target.Headers = source.Headers;
}
// Umbraco.Code.MapAll
private void Map(WebhookLog source, WebhookLogViewModel target, MapperContext context)
{

View File

@@ -0,0 +1,8 @@
namespace Umbraco.Cms.Api.Management.ViewModels.Webhook;
public class CreateWebhookRequestModel : WebhookModelBase
{
public Guid? Id { get; set; }
public string[] Events { get; set; } = Array.Empty<string>();
}

View File

@@ -0,0 +1,14 @@
namespace Umbraco.Cms.Api.Management.ViewModels.Webhook.Item;
public class WebhookItemResponseModel
{
public bool Enabled { get; set; } = true;
public string Name { get; set; } = string.Empty;
public string Events { get; set; } = string.Empty;
public string Url { get; set; } = string.Empty;
public string Types { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.Webhook;
public class UpdateWebhookRequestModel : WebhookModelBase
{
public string[] Events { get; set; } = Array.Empty<string>();
}

View File

@@ -0,0 +1,10 @@
namespace Umbraco.Cms.Api.Management.ViewModels.Webhook;
public class WebhookEventResponseModel
{
public string EventName { get; set; } = string.Empty;
public string EventType { get; set; } = string.Empty;
public string Alias { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,15 @@
using System.ComponentModel.DataAnnotations;
namespace Umbraco.Cms.Api.Management.ViewModels.Webhook;
public class WebhookModelBase
{
public bool Enabled { get; set; } = true;
[Required]
public string Url { get; set; } = string.Empty;
public Guid[] ContentTypeKeys { get; set; } = Array.Empty<Guid>();
public IDictionary<string, string> Headers { get; set; } = new Dictionary<string, string>();
}

View File

@@ -0,0 +1,8 @@
namespace Umbraco.Cms.Api.Management.ViewModels.Webhook;
public class WebhookResponseModel : WebhookModelBase
{
public Guid Id { get; set; }
public WebhookEventResponseModel[] Events { get; set; } = Array.Empty<WebhookEventResponseModel>();
}

View File

@@ -1,4 +1,4 @@
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Core.Persistence.Repositories;
@@ -20,30 +20,40 @@ public interface IWebhookRepository
Task<IWebhook> CreateAsync(IWebhook webhook);
/// <summary>
/// Gets a webhook by key
/// Gets a webhook by key.
/// </summary>
/// <param name="key">The key of the webhook which will be retrieved.</param>
/// <returns>The <see cref="IWebhook" /> webhook with the given key.</returns>
Task<IWebhook?> GetAsync(Guid key);
/// <summary>
/// Gets a webhook by key
/// Gets webhooks by keys.
/// </summary>
/// <param name="keys">The alias of an event, which is referenced by a webhook.</param>
/// <returns>
/// A paged model of <see cref="IWebhook" />.
/// </returns>
Task<PagedModel<IWebhook>> GetByIdsAsync(IEnumerable<Guid> keys) =>
throw new NotImplementedException();
/// <summary>
/// Gets a webhook by key.
/// </summary>
/// <param name="alias">The alias of an event, which is referenced by a webhook.</param>
/// <returns>
/// A paged model of <see cref="IWebhook" />
/// A paged model of <see cref="IWebhook" />.
/// </returns>
Task<PagedModel<IWebhook>> GetByAliasAsync(string alias);
/// <summary>
/// Gets a webhook by key
/// Gets a webhook by key.
/// </summary>
/// <param name="webhook">The webhook to be deleted.</param>
/// <returns><placeholder>A <see cref="Task"/> representing the asynchronous operation.</placeholder></returns>
Task DeleteAsync(IWebhook webhook);
/// <summary>
/// Updates a given webhook
/// Updates a given webhook.
/// </summary>
/// <param name="webhook">The webhook to be updated.</param>
/// <returns>The updated <see cref="IWebhook" /> webhook.</returns>

View File

@@ -29,6 +29,13 @@ public interface IWebhookService
/// <param name="key">The unique key of the webhook.</param>
Task<IWebhook?> GetAsync(Guid key);
/// <summary>
/// Gets all webhooks with the given keys.
/// </summary>
/// <returns>An enumerable list of <see cref="IWebhook" /> objects.</returns>
Task<IEnumerable<IWebhook?>> GetMultipleAsync(IEnumerable<Guid> keys)
=> throw new NotImplementedException();
/// <summary>
/// Gets all webhooks.
/// </summary>

View File

@@ -135,6 +135,16 @@ public class WebhookService : IWebhookService
return webhook;
}
/// <inheritdoc />
public async Task<IEnumerable<IWebhook?>> GetMultipleAsync(IEnumerable<Guid> keys)
{
using ICoreScope scope = _provider.CreateCoreScope();
PagedModel<IWebhook> webhooks = await _webhookRepository.GetByIdsAsync(keys);
scope.Complete();
return webhooks.Items;
}
/// <inheritdoc />
public async Task<PagedModel<IWebhook>> GetAllAsync(int skip, int take)
{

View File

@@ -1,4 +1,4 @@
using NPoco;
using NPoco;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
@@ -58,6 +58,24 @@ public class WebhookRepository : IWebhookRepository
return webhookDto is null ? null : await DtoToEntity(webhookDto);
}
public async Task<PagedModel<IWebhook>> GetByIdsAsync(IEnumerable<Guid> keys)
{
Sql<ISqlContext>? sql = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql()
.SelectAll()
.From<WebhookDto>()
.InnerJoin<Webhook2EventsDto>()
.On<WebhookDto, Webhook2EventsDto>(left => left.Id, right => right.WebhookId)
.WhereIn<Webhook2EventsDto>(x => x.WebhookId, keys);
List<WebhookDto>? webhookDtos = await _scopeAccessor.AmbientScope?.Database.FetchAsync<WebhookDto>(sql)!;
return new PagedModel<IWebhook>
{
Items = await DtosToEntities(webhookDtos),
Total = webhookDtos.Count,
};
}
public async Task<PagedModel<IWebhook>> GetByAliasAsync(string alias)
{
Sql<ISqlContext>? sql = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql()

View File

@@ -581,10 +581,6 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
"mediaPickerThreeBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl<MediaPickerThreeController>(
controller => controller.UploadMedia(null!))
},
{
"webhooksApiBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl<WebhookController>(
controller => controller.GetAll(0, 0))
},
}
},
{

View File

@@ -1,112 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Core.Webhooks;
using Umbraco.Cms.Web.BackOffice.Services;
using Umbraco.Cms.Web.Common.Attributes;
using Umbraco.Cms.Web.Common.Authorization;
using Umbraco.Cms.Web.Common.Models;
namespace Umbraco.Cms.Web.BackOffice.Controllers;
[PluginController(Constants.Web.Mvc.BackOfficeApiArea)]
[Authorize(Policy = AuthorizationPolicies.TreeAccessWebhooks)]
public class WebhookController : UmbracoAuthorizedJsonController
{
private readonly IWebhookService _webhookService;
private readonly IUmbracoMapper _umbracoMapper;
private readonly WebhookEventCollection _webhookEventCollection;
private readonly IWebhookLogService _webhookLogService;
private readonly IWebhookPresentationFactory _webhookPresentationFactory;
public WebhookController(IWebhookService webhookService, IUmbracoMapper umbracoMapper, WebhookEventCollection webhookEventCollection, IWebhookLogService webhookLogService, IWebhookPresentationFactory webhookPresentationFactory)
{
_webhookService = webhookService;
_umbracoMapper = umbracoMapper;
_webhookEventCollection = webhookEventCollection;
_webhookLogService = webhookLogService;
_webhookPresentationFactory = webhookPresentationFactory;
}
[HttpGet]
public async Task<IActionResult> GetAll(int skip = 0, int take = int.MaxValue)
{
PagedModel<IWebhook> webhooks = await _webhookService.GetAllAsync(skip, take);
IEnumerable<WebhookViewModel> webhookViewModels = webhooks.Items.Select(_webhookPresentationFactory.Create);
return Ok(webhookViewModels);
}
[HttpPut]
public async Task<IActionResult> Update(WebhookViewModel webhookViewModel)
{
IWebhook webhook = _umbracoMapper.Map<IWebhook>(webhookViewModel)!;
Attempt<IWebhook, WebhookOperationStatus> result = await _webhookService.UpdateAsync(webhook);
return result.Success ? Ok(_webhookPresentationFactory.Create(webhook)) : WebhookOperationStatusResult(result.Status);
}
[HttpPost]
public async Task<IActionResult> Create(WebhookViewModel webhookViewModel)
{
IWebhook webhook = _umbracoMapper.Map<IWebhook>(webhookViewModel)!;
Attempt<IWebhook, WebhookOperationStatus> result = await _webhookService.CreateAsync(webhook);
return result.Success ? Ok(_webhookPresentationFactory.Create(webhook)) : WebhookOperationStatusResult(result.Status);
}
[HttpGet]
public async Task<IActionResult> GetByKey(Guid key)
{
IWebhook? webhook = await _webhookService.GetAsync(key);
return webhook is null ? NotFound() : Ok(_webhookPresentationFactory.Create(webhook));
}
[HttpDelete]
public async Task<IActionResult> Delete(Guid key)
{
Attempt<IWebhook?, WebhookOperationStatus> result = await _webhookService.DeleteAsync(key);
return result.Success ? Ok() : WebhookOperationStatusResult(result.Status);
}
[HttpGet]
public IActionResult GetEvents()
{
List<WebhookEventViewModel> viewModels = _umbracoMapper.MapEnumerable<IWebhookEvent, WebhookEventViewModel>(_webhookEventCollection.AsEnumerable());
return Ok(viewModels);
}
[HttpGet]
public async Task<IActionResult> GetLogs(int skip = 0, int take = int.MaxValue)
{
PagedModel<WebhookLog> logs = await _webhookLogService.Get(skip, take);
List<WebhookLogViewModel> mappedLogs = _umbracoMapper.MapEnumerable<WebhookLog, WebhookLogViewModel>(logs.Items);
return Ok(new PagedResult<WebhookLogViewModel>(logs.Total, 0, 0)
{
Items = mappedLogs,
});
}
private IActionResult WebhookOperationStatusResult(WebhookOperationStatus status) =>
status switch
{
WebhookOperationStatus.CancelledByNotification => ValidationProblem(new SimpleNotificationModel(new BackOfficeNotification[]
{
new("Cancelled by notification", "The operation was cancelled by a notification", NotificationStyle.Error),
})),
WebhookOperationStatus.NotFound => NotFound("Could not find the webhook"),
WebhookOperationStatus.NoEvents => ValidationProblem(new SimpleNotificationModel(new BackOfficeNotification[]
{
new("No events", "The webhook does not have any events", NotificationStyle.Error),
})),
_ => StatusCode(StatusCodes.Status500InternalServerError),
};
}

View File

@@ -94,8 +94,6 @@ public static partial class UmbracoBuilderExtensions
builder.Services.AddSingleton<BackOfficeServerVariables>();
builder.Services.AddScoped<BackOfficeSessionIdValidator>();
builder.Services.AddScoped<BackOfficeSecurityStampValidator>();
builder.WithCollectionBuilder<MapDefinitionCollectionBuilder>().Add<WebhookMapDefinition>();
// register back office trees
// the collection builder only accepts types inheriting from TreeControllerBase
// and will filter out those that are not attributed with TreeAttribute
@@ -122,7 +120,6 @@ public static partial class UmbracoBuilderExtensions
builder.Services.AddUnique<IConflictingRouteService, ConflictingRouteService>();
builder.Services.AddSingleton<UnhandledExceptionLoggerMiddleware>();
builder.Services.AddTransient<BlockGridSampleHelper>();
builder.Services.AddUnique<IWebhookPresentationFactory, WebhookPresentationFactory>();
return builder;
}

View File

@@ -1,10 +0,0 @@
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Web.Common.Models;
namespace Umbraco.Cms.Web.BackOffice.Services;
[Obsolete("Will be moved to a new namespace in V14")]
public interface IWebhookPresentationFactory
{
WebhookViewModel Create(IWebhook webhook);
}

View File

@@ -1,17 +0,0 @@
using System.Runtime.Serialization;
using Umbraco.Cms.Core.Webhooks;
namespace Umbraco.Cms.Web.Common.Models;
[DataContract]
public class WebhookEventViewModel
{
[DataMember(Name = "eventName")]
public string EventName { get; set; } = string.Empty;
[DataMember(Name = "eventType")]
public string EventType { get; set; } = string.Empty;
[DataMember(Name = "alias")]
public string Alias { get; set; } = string.Empty;
}

View File

@@ -1,28 +0,0 @@
using System.Runtime.Serialization;
namespace Umbraco.Cms.Web.Common.Models;
[DataContract]
public class WebhookViewModel
{
[DataMember(Name = "id")]
public int Id { get; set; }
[DataMember(Name = "key")]
public Guid? Key { get; set; }
[DataMember(Name = "url")]
public string Url { get; set; } = string.Empty;
[DataMember(Name = "events")]
public WebhookEventViewModel[] Events { get; set; } = Array.Empty<WebhookEventViewModel>();
[DataMember(Name = "contentTypeKeys")]
public Guid[] ContentTypeKeys { get; set; } = Array.Empty<Guid>();
[DataMember(Name = "enabled")]
public bool Enabled { get; set; }
[DataMember(Name = "headers")]
public IDictionary<string, string> Headers { get; set; } = new Dictionary<string, string>();
}