Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
9a00994
Add webhook to management api
bjarnef Nov 6, 2023
3a7f7ce
Update webhook controllers
bjarnef Nov 6, 2023
e0b2a15
Add ByKey webhook controller
bjarnef Nov 6, 2023
f6b690e
Fix typo
bjarnef Nov 7, 2023
651913f
Fix typo
bjarnef Nov 7, 2023
5387c14
Update multiple webhooks
bjarnef Nov 7, 2023
547f64d
Update using
bjarnef Nov 7, 2023
252f7cb
Merge branch 'v14/dev' of https://github.com/umbraco/Umbraco-CMS into…
bjarnef Nov 15, 2023
9636453
Remove duplicate constant after merge
bjarnef Nov 15, 2023
f0bab65
Fix typo in file name
bjarnef Nov 15, 2023
af0f2fb
Update casing of IWebhookService
bjarnef Nov 15, 2023
1e86abd
Fix typo
bjarnef Nov 15, 2023
15fb5a3
Merge branch 'v14/dev' of https://github.com/umbraco/Umbraco-CMS into…
bjarnef Nov 16, 2023
94142e2
Use Webhook entity type
bjarnef Nov 16, 2023
6cdce7a
Fix ambiguous reference
bjarnef Nov 16, 2023
e31a263
Merge branch 'v14/dev' of https://github.com/umbraco/Umbraco-CMS into…
bjarnef Dec 29, 2023
cb35d1d
Update webhook mapping
bjarnef Jan 1, 2024
6d0398f
Merge branch 'v14/dev' of https://github.com/umbraco/Umbraco-CMS into…
bjarnef Feb 19, 2024
7b48a71
Update after change of CreatedAtAction
bjarnef Feb 19, 2024
6f4675e
Use CreatedAtId instead
bjarnef Feb 19, 2024
a17496a
Update src/Umbraco.Cms.Api.Management/Controllers/Webhook/ByKeyWebhoo…
bjarnef Feb 23, 2024
deb01d3
Update src/Umbraco.Cms.Api.Management/Controllers/Webhook/ByKeyWebhoo…
bjarnef Feb 23, 2024
3a675f0
Update src/Umbraco.Cms.Api.Management/ViewModels/Webhook/CreateWebhoo…
bjarnef Feb 23, 2024
8180ede
Update src/Umbraco.Cms.Api.Management/Controllers/Webhook/DeleteWebho…
bjarnef Feb 23, 2024
268babf
Update src/Umbraco.Cms.Api.Management/Controllers/Webhook/CreateWebho…
bjarnef Feb 23, 2024
5005138
Add Guid to WebhookResponseModel
bjarnef Feb 23, 2024
b5969a3
Cleanup
bjarnef Feb 23, 2024
ac7b3c1
Add Auth
Zeegaan Feb 26, 2024
e2461b6
Move webhook logic from backoffice to management api
Zeegaan Feb 26, 2024
7d7f2f1
Add mapping
Zeegaan Feb 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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
{
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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());
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ public static IUmbracoBuilder AddUmbracoManagementApi(this IUmbracoBuilder build
.AddScripts()
.AddPartialViews()
.AddStylesheets()
.AddWebhooks()
.AddServer()
.AddCorsPolicy()
.AddBackOfficeAuthentication()
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -1,37 +1,48 @@
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
{
private readonly WebhookEventCollection _webhookEventCollection;

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,
Expand Down
Loading