diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Webhook/ByKeyWebhookController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Webhook/ByKeyWebhookController.cs new file mode 100644 index 000000000000..a5b1edb9c3b7 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Webhook/ByKeyWebhookController.cs @@ -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 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); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Webhook/CreateWebhookController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Webhook/CreateWebhookController.cs new file mode 100644 index 000000000000..80c5418ff709 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Webhook/CreateWebhookController.cs @@ -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 Create(CreateWebhookRequestModel createWebhookRequestModel) + { + IWebhook created = _umbracoMapper.Map(createWebhookRequestModel)!; + + Attempt result = await _webhookService.CreateAsync(created); + + return result.Success + ? CreatedAtId(controller => nameof(controller.ByKey), result.Result!.Key) + : WebhookOperationStatusResult(result.Status); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Webhook/DeleteWebhookController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Webhook/DeleteWebhookController.cs new file mode 100644 index 000000000000..103bfe645c6f --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Webhook/DeleteWebhookController.cs @@ -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 Delete(Guid id) + { + Attempt result = await _webhookService.DeleteAsync(id); + + return result.Success + ? Ok() + : WebhookOperationStatusResult(result.Status); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Webhook/Item/ItemsWebhookEntityController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Webhook/Item/ItemsWebhookEntityController.cs new file mode 100644 index 000000000000..4f450465cc56 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Webhook/Item/ItemsWebhookEntityController.cs @@ -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), StatusCodes.Status200OK)] + public async Task Items([FromQuery(Name = "ids")] HashSet ids) + { + IEnumerable webhooks = await _webhookService.GetMultipleAsync(ids); + List entityResponseModels = _mapper.MapEnumerable(webhooks); + return Ok(entityResponseModels); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Webhook/Item/WebhookEntityControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Webhook/Item/WebhookEntityControllerBase.cs new file mode 100644 index 000000000000..1982cd898d3d --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Webhook/Item/WebhookEntityControllerBase.cs @@ -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 +{ +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Webhook/UpdateWebhookController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Webhook/UpdateWebhookController.cs new file mode 100644 index 000000000000..a74a4eb27af0 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Webhook/UpdateWebhookController.cs @@ -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 Update(Guid id, UpdateWebhookRequestModel updateWebhookRequestModel) + { + IWebhook? current = await _webhookService.GetAsync(id); + if (current is null) + { + return WebhookNotFound(); + } + + IWebhook updated = _umbracoMapper.Map(updateWebhookRequestModel, current); + + Attempt result = await _webhookService.UpdateAsync(updated); + + return result.Success + ? Ok() + : WebhookOperationStatusResult(result.Status); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Webhook/WebhookControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Webhook/WebhookControllerBase.cs new file mode 100644 index 000000000000..30f9bcb5fdd8 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Webhook/WebhookControllerBase.cs @@ -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()); +} diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeAuthPolicyBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeAuthPolicyBuilderExtensions.cs index b996a426ab12..601bc7f8a156 100644 --- a/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeAuthPolicyBuilderExtensions.cs +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeAuthPolicyBuilderExtensions.cs @@ -98,6 +98,7 @@ void AddPolicy(string policyName, string claimType, params string[] allowedClaim 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 diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilderExtensions.cs index 5a2f10fb168d..f36f88b38412 100644 --- a/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilderExtensions.cs @@ -52,6 +52,7 @@ public static IUmbracoBuilder AddUmbracoManagementApi(this IUmbracoBuilder build .AddScripts() .AddPartialViews() .AddStylesheets() + .AddWebhooks() .AddServer() .AddCorsPolicy() .AddBackOfficeAuthentication() diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/WebhookBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/WebhookBuilderExtensions.cs new file mode 100644 index 000000000000..85c85e3915d4 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/WebhookBuilderExtensions.cs @@ -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().Add(); + builder.Services.AddUnique(); + + return builder; + } +} diff --git a/src/Umbraco.Cms.Api.Management/Factories/IWebhookPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/IWebhookPresentationFactory.cs new file mode 100644 index 000000000000..113d739bfb85 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Factories/IWebhookPresentationFactory.cs @@ -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); +} diff --git a/src/Umbraco.Web.BackOffice/Services/WebhookPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/WebhookPresentationFactory.cs similarity index 50% rename from src/Umbraco.Web.BackOffice/Services/WebhookPresentationFactory.cs rename to src/Umbraco.Cms.Api.Management/Factories/WebhookPresentationFactory.cs index bf33716c08e4..b55fd86fce41 100644 --- a/src/Umbraco.Web.BackOffice/Services/WebhookPresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/WebhookPresentationFactory.cs @@ -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, diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Item/ItemTypeMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/Item/ItemTypeMapDefinition.cs index 78dd870217c8..782f5033707e 100644 --- a/src/Umbraco.Cms.Api.Management/Mapping/Item/ItemTypeMapDefinition.cs +++ b/src/Umbraco.Cms.Api.Management/Mapping/Item/ItemTypeMapDefinition.cs @@ -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.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; @@ -32,6 +33,7 @@ public void DefineMaps(IUmbracoMapper mapper) mapper.Define((_, _) => new RelationTypeItemResponseModel(), Map); mapper.Define((_, _) => new UserItemResponseModel(), Map); mapper.Define((_, _) => new UserGroupItemResponseModel(), Map); + mapper.Define((_, _) => new WebhookItemResponseModel(), Map); } // Umbraco.Code.MapAll @@ -117,4 +119,14 @@ private static void Map(IUserGroup source, UserGroupItemResponseModel target, Ma 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); + } } diff --git a/src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/Webhook/WebhookMapDefinition.cs similarity index 67% rename from src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs rename to src/Umbraco.Cms.Api.Management/Mapping/Webhook/WebhookMapDefinition.cs index 1d1558026cb0..cc89a5b27fe2 100644 --- a/src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs +++ b/src/Umbraco.Cms.Api.Management/Mapping/Webhook/WebhookMapDefinition.cs @@ -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(), - StaticServiceProvider.Instance.GetRequiredService() - ) - { - - } - public WebhookMapDefinition(IHostingEnvironment hostingEnvironment, ILocalizedTextService localizedTextService) { _hostingEnvironment = hostingEnvironment; @@ -31,29 +21,39 @@ public WebhookMapDefinition(IHostingEnvironment hostingEnvironment, ILocalizedTe public void DefineMaps(IUmbracoMapper mapper) { - mapper.Define((_, _) => new Webhook(string.Empty), Map); - mapper.Define((_, _) => new WebhookEventViewModel(), Map); + mapper.Define((_, _) => new WebhookEventResponseModel(), Map); mapper.Define((_, _) => new WebhookLogViewModel(), Map); + mapper.Define((_, _) => new Webhook(string.Empty), Map); + mapper.Define((_, _) => new Webhook(string.Empty), Map); } - // Umbraco.Code.MapAll -CreateDate -DeleteDate -Id -Key -UpdateDate - private void Map(WebhookViewModel source, IWebhook target, MapperContext context) + // Umbraco.Code.MapAll + 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.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.ContentTypeKeys = source.ContentTypeKeys; + target.Events = source.Events; target.Headers = source.Headers; + target.Key = source.Id ?? Guid.NewGuid(); } - // Umbraco.Code.MapAll - private void Map(IWebhookEvent source, WebhookEventViewModel target, MapperContext context) + // Umbraco.Code.MapAll -CreateDate -DeleteDate -Id -UpdateDate -Key + private void Map(UpdateWebhookRequestModel source, IWebhook target, MapperContext context) { - target.EventName = source.EventName; - target.EventType = source.EventType; - target.Alias = source.Alias; + target.Url = source.Url; + target.Enabled = source.Enabled; + target.ContentTypeKeys = source.ContentTypeKeys; + target.Events = source.Events; + target.Headers = source.Headers; } // Umbraco.Code.MapAll diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Webhook/CreateWebhookRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Webhook/CreateWebhookRequestModel.cs new file mode 100644 index 000000000000..3bfbb2102a26 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Webhook/CreateWebhookRequestModel.cs @@ -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(); +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Webhook/Item/WebhookItemResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Webhook/Item/WebhookItemResponseModel.cs new file mode 100644 index 000000000000..23daaf35d9ef --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Webhook/Item/WebhookItemResponseModel.cs @@ -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; +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Webhook/UpdateWebhookRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Webhook/UpdateWebhookRequestModel.cs new file mode 100644 index 000000000000..033863600136 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Webhook/UpdateWebhookRequestModel.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.Webhook; + +public class UpdateWebhookRequestModel : WebhookModelBase +{ + public string[] Events { get; set; } = Array.Empty(); +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Webhook/WebhookEventResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Webhook/WebhookEventResponseModel.cs new file mode 100644 index 000000000000..7dfef197cfb3 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Webhook/WebhookEventResponseModel.cs @@ -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; +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Webhook/WebhookModelBase.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Webhook/WebhookModelBase.cs new file mode 100644 index 000000000000..10f497c54359 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Webhook/WebhookModelBase.cs @@ -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(); + + public IDictionary Headers { get; set; } = new Dictionary(); +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Webhook/WebhookResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Webhook/WebhookResponseModel.cs new file mode 100644 index 000000000000..f2cc9640d6e9 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Webhook/WebhookResponseModel.cs @@ -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(); +} diff --git a/src/Umbraco.Core/Persistence/Repositories/IWebhookRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IWebhookRepository.cs index 5fe0530b73d3..c75bb1b50f7c 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IWebhookRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IWebhookRepository.cs @@ -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 CreateAsync(IWebhook webhook); /// - /// Gets a webhook by key + /// Gets a webhook by key. /// /// The key of the webhook which will be retrieved. /// The webhook with the given key. Task GetAsync(Guid key); /// - /// Gets a webhook by key + /// Gets webhooks by keys. + /// + /// The alias of an event, which is referenced by a webhook. + /// + /// A paged model of . + /// + Task> GetByIdsAsync(IEnumerable keys) => + throw new NotImplementedException(); + + /// + /// Gets a webhook by key. /// /// The alias of an event, which is referenced by a webhook. /// - /// A paged model of + /// A paged model of . /// Task> GetByAliasAsync(string alias); /// - /// Gets a webhook by key + /// Gets a webhook by key. /// /// The webhook to be deleted. /// A representing the asynchronous operation. Task DeleteAsync(IWebhook webhook); /// - /// Updates a given webhook + /// Updates a given webhook. /// /// The webhook to be updated. /// The updated webhook. diff --git a/src/Umbraco.Core/Services/IWebhookService.cs b/src/Umbraco.Core/Services/IWebhookService.cs index 2fcd3eef94f1..8d6ade4b7652 100644 --- a/src/Umbraco.Core/Services/IWebhookService.cs +++ b/src/Umbraco.Core/Services/IWebhookService.cs @@ -29,6 +29,13 @@ public interface IWebhookService /// The unique key of the webhook. Task GetAsync(Guid key); + /// + /// Gets all webhooks with the given keys. + /// + /// An enumerable list of objects. + Task> GetMultipleAsync(IEnumerable keys) + => throw new NotImplementedException(); + /// /// Gets all webhooks. /// diff --git a/src/Umbraco.Core/Services/WebhookService.cs b/src/Umbraco.Core/Services/WebhookService.cs index 5f660eb37ddb..ac3fabd97386 100644 --- a/src/Umbraco.Core/Services/WebhookService.cs +++ b/src/Umbraco.Core/Services/WebhookService.cs @@ -135,6 +135,16 @@ public async Task> UpdateAsync(IWebhoo return webhook; } + /// + public async Task> GetMultipleAsync(IEnumerable keys) + { + using ICoreScope scope = _provider.CreateCoreScope(); + PagedModel webhooks = await _webhookRepository.GetByIdsAsync(keys); + scope.Complete(); + + return webhooks.Items; + } + /// public async Task> GetAllAsync(int skip, int take) { diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs index 7358374f3bfb..910327b50b74 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs @@ -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 async Task CreateAsync(IWebhook webhook) return webhookDto is null ? null : await DtoToEntity(webhookDto); } + public async Task> GetByIdsAsync(IEnumerable keys) + { + Sql? sql = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql() + .SelectAll() + .From() + .InnerJoin() + .On(left => left.Id, right => right.WebhookId) + .WhereIn(x => x.WebhookId, keys); + + List? webhookDtos = await _scopeAccessor.AmbientScope?.Database.FetchAsync(sql)!; + + return new PagedModel + { + Items = await DtosToEntities(webhookDtos), + Total = webhookDtos.Count, + }; + } + public async Task> GetByAliasAsync(string alias) { Sql? sql = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql() diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs index b48261fe9b82..fe1ee521c488 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs @@ -581,10 +581,6 @@ internal async Task> GetServerVariablesAsync() "mediaPickerThreeBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl( controller => controller.UploadMedia(null!)) }, - { - "webhooksApiBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl( - controller => controller.GetAll(0, 0)) - }, } }, { diff --git a/src/Umbraco.Web.BackOffice/Controllers/WebhookController.cs b/src/Umbraco.Web.BackOffice/Controllers/WebhookController.cs deleted file mode 100644 index ac230ccd4c93..000000000000 --- a/src/Umbraco.Web.BackOffice/Controllers/WebhookController.cs +++ /dev/null @@ -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 GetAll(int skip = 0, int take = int.MaxValue) - { - PagedModel webhooks = await _webhookService.GetAllAsync(skip, take); - - IEnumerable webhookViewModels = webhooks.Items.Select(_webhookPresentationFactory.Create); - - return Ok(webhookViewModels); - } - - [HttpPut] - public async Task Update(WebhookViewModel webhookViewModel) - { - IWebhook webhook = _umbracoMapper.Map(webhookViewModel)!; - - Attempt result = await _webhookService.UpdateAsync(webhook); - return result.Success ? Ok(_webhookPresentationFactory.Create(webhook)) : WebhookOperationStatusResult(result.Status); - } - - [HttpPost] - public async Task Create(WebhookViewModel webhookViewModel) - { - IWebhook webhook = _umbracoMapper.Map(webhookViewModel)!; - Attempt result = await _webhookService.CreateAsync(webhook); - return result.Success ? Ok(_webhookPresentationFactory.Create(webhook)) : WebhookOperationStatusResult(result.Status); - } - - [HttpGet] - public async Task GetByKey(Guid key) - { - IWebhook? webhook = await _webhookService.GetAsync(key); - - return webhook is null ? NotFound() : Ok(_webhookPresentationFactory.Create(webhook)); - } - - [HttpDelete] - public async Task Delete(Guid key) - { - Attempt result = await _webhookService.DeleteAsync(key); - return result.Success ? Ok() : WebhookOperationStatusResult(result.Status); - } - - [HttpGet] - public IActionResult GetEvents() - { - List viewModels = _umbracoMapper.MapEnumerable(_webhookEventCollection.AsEnumerable()); - return Ok(viewModels); - } - - [HttpGet] - public async Task GetLogs(int skip = 0, int take = int.MaxValue) - { - PagedModel logs = await _webhookLogService.Get(skip, take); - List mappedLogs = _umbracoMapper.MapEnumerable(logs.Items); - return Ok(new PagedResult(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), - - }; -} diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs index 1796b03ff8df..8e29c1ed45d3 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs @@ -94,8 +94,6 @@ public static IUmbracoBuilder AddBackOfficeCore(this IUmbracoBuilder builder) builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddScoped(); - builder.WithCollectionBuilder().Add(); - // 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 IUmbracoBuilder AddBackOfficeCore(this IUmbracoBuilder builder) builder.Services.AddUnique(); builder.Services.AddSingleton(); builder.Services.AddTransient(); - builder.Services.AddUnique(); return builder; } diff --git a/src/Umbraco.Web.BackOffice/Services/IWebhookPresentationFactory.cs b/src/Umbraco.Web.BackOffice/Services/IWebhookPresentationFactory.cs deleted file mode 100644 index 77c2104a8c9e..000000000000 --- a/src/Umbraco.Web.BackOffice/Services/IWebhookPresentationFactory.cs +++ /dev/null @@ -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); -} diff --git a/src/Umbraco.Web.Common/Models/WebhookEventViewModel.cs b/src/Umbraco.Web.Common/Models/WebhookEventViewModel.cs deleted file mode 100644 index 05243d4eb65f..000000000000 --- a/src/Umbraco.Web.Common/Models/WebhookEventViewModel.cs +++ /dev/null @@ -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; -} diff --git a/src/Umbraco.Web.Common/Models/WebhookViewModel.cs b/src/Umbraco.Web.Common/Models/WebhookViewModel.cs deleted file mode 100644 index a1e581ff2243..000000000000 --- a/src/Umbraco.Web.Common/Models/WebhookViewModel.cs +++ /dev/null @@ -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(); - - [DataMember(Name = "contentTypeKeys")] - public Guid[] ContentTypeKeys { get; set; } = Array.Empty(); - - [DataMember(Name = "enabled")] - public bool Enabled { get; set; } - - [DataMember(Name = "headers")] - public IDictionary Headers { get; set; } = new Dictionary(); -}