diff --git a/Directory.Build.props b/Directory.Build.props
index f9fb83457d..0df4f75e8a 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -1,7 +1,7 @@
- net8.0
+ net9.0
Umbraco HQ
Umbraco
Copyright © Umbraco $([System.DateTime]::Today.ToString('yyyy'))
@@ -30,7 +30,7 @@
false
- true
+ false
14.0.0
true
true
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 47df4cf510..e60b2c718a 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -5,35 +5,35 @@
-
+
-
-
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -47,7 +47,7 @@
-
+
@@ -58,24 +58,24 @@
-
-
-
-
+
+
+
+
-
-
-
+
+
+
-
-
+
+
-
-
-
+
+
+
-
+
@@ -84,10 +84,15 @@
-
+
-
+
+
+
+
+
+
\ No newline at end of file
diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml
index 9e7207bf07..b7fadde6d5 100644
--- a/build/azure-pipelines.yml
+++ b/build/azure-pipelines.yml
@@ -287,8 +287,8 @@ stages:
displayName: Integration Tests (SQLite)
strategy:
matrix:
- Windows:
- vmImage: 'windows-latest'
+# Windows:
+# vmImage: 'windows-latest'
Linux:
vmImage: 'ubuntu-latest'
macOS:
diff --git a/global.json b/global.json
index e972eb192a..9c2a135743 100644
--- a/global.json
+++ b/global.json
@@ -1,6 +1,7 @@
{
"sdk": {
- "version": "8.0.300",
- "rollForward": "latestFeature"
+ "version": "9.0.100-rc.1.24452.12",
+ "rollForward": "latestFeature",
+ "allowPrerelease": true
}
}
diff --git a/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderAuthExtensions.cs b/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderAuthExtensions.cs
index 98068791af..f34fc2dde9 100644
--- a/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderAuthExtensions.cs
+++ b/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderAuthExtensions.cs
@@ -54,10 +54,14 @@ public static class UmbracoBuilderAuthExtensions
.RequireProofKeyForCodeExchange()
.AllowRefreshTokenFlow();
+ // Enable the client credentials flow.
+ options.AllowClientCredentialsFlow();
+
// Register the ASP.NET Core host and configure for custom authentication endpoint.
options
.UseAspNetCore()
.EnableAuthorizationEndpointPassthrough()
+ .EnableTokenEndpointPassthrough()
.EnableLogoutEndpointPassthrough();
// Enable reference tokens
diff --git a/src/Umbraco.Cms.Api.Common/OpenApi/SchemaIdHandler.cs b/src/Umbraco.Cms.Api.Common/OpenApi/SchemaIdHandler.cs
index c08e0be19a..6e96cbed42 100644
--- a/src/Umbraco.Cms.Api.Common/OpenApi/SchemaIdHandler.cs
+++ b/src/Umbraco.Cms.Api.Common/OpenApi/SchemaIdHandler.cs
@@ -36,7 +36,7 @@ public class SchemaIdHandler : ISchemaIdHandler
// first grab the "non-generic" part of any generic type name (i.e. "PagedViewModel`1" becomes "PagedViewModel")
.Split('`').First()
// then remove the "ViewModel" postfix from type names
- .TrimEnd("ViewModel");
+ .TrimEndExact("ViewModel");
private string HandleGenerics(string name, Type type)
{
diff --git a/src/Umbraco.Cms.Api.Delivery/Controllers/Security/MemberController.cs b/src/Umbraco.Cms.Api.Delivery/Controllers/Security/MemberController.cs
index 80b42611b8..a5b085073b 100644
--- a/src/Umbraco.Cms.Api.Delivery/Controllers/Security/MemberController.cs
+++ b/src/Umbraco.Cms.Api.Delivery/Controllers/Security/MemberController.cs
@@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OpenIddict.Abstractions;
@@ -12,6 +13,7 @@ using OpenIddict.Server.AspNetCore;
using Umbraco.Cms.Api.Delivery.Routing;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
+using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Web.Common.Security;
using Umbraco.Extensions;
@@ -25,22 +27,47 @@ namespace Umbraco.Cms.Api.Delivery.Controllers.Security;
[ApiExplorerSettings(IgnoreApi = true)]
public class MemberController : DeliveryApiControllerBase
{
- private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IMemberSignInManager _memberSignInManager;
private readonly IMemberManager _memberManager;
+ private readonly IMemberClientCredentialsManager _memberClientCredentialsManager;
private readonly DeliveryApiSettings _deliveryApiSettings;
private readonly ILogger _logger;
+
+ [Obsolete("Please use the non-obsolete constructor. Will be removed in V16.")]
public MemberController(
IHttpContextAccessor httpContextAccessor,
IMemberSignInManager memberSignInManager,
IMemberManager memberManager,
IOptions deliveryApiSettings,
ILogger logger)
+ : this(memberSignInManager, memberManager, StaticServiceProvider.Instance.GetRequiredService(), deliveryApiSettings, logger)
+ {
+ }
+
+ [Obsolete("Please use the non-obsolete constructor. Will be removed in V16.")]
+ public MemberController(
+ IHttpContextAccessor httpContextAccessor,
+ IMemberSignInManager memberSignInManager,
+ IMemberManager memberManager,
+ IMemberClientCredentialsManager memberClientCredentialsManager,
+ IOptions deliveryApiSettings,
+ ILogger logger)
+ : this(memberSignInManager, memberManager, memberClientCredentialsManager, deliveryApiSettings, logger)
+ {
+ }
+
+ [ActivatorUtilitiesConstructor]
+ public MemberController(
+ IMemberSignInManager memberSignInManager,
+ IMemberManager memberManager,
+ IMemberClientCredentialsManager memberClientCredentialsManager,
+ IOptions deliveryApiSettings,
+ ILogger logger)
{
- _httpContextAccessor = httpContextAccessor;
_memberSignInManager = memberSignInManager;
_memberManager = memberManager;
+ _memberClientCredentialsManager = memberClientCredentialsManager;
_logger = logger;
_deliveryApiSettings = deliveryApiSettings.Value;
}
@@ -49,25 +76,31 @@ public class MemberController : DeliveryApiControllerBase
[MapToApiVersion("1.0")]
public async Task Authorize()
{
- // in principle this is not necessary for now, since the member application has been removed, thus making
- // the member client ID invalid for the authentication code flow. However, if we ever add additional flows
- // to the API, we should perform this check, so we might as well include it upfront.
- if (_deliveryApiSettings.MemberAuthorizationIsEnabled() is false)
+ // the Authorize endpoint is not allowed unless authorization code flow is enabled.
+ if (_deliveryApiSettings.MemberAuthorization?.AuthorizationCodeFlow?.Enabled is not true)
{
- return BadRequest("Member authorization is not allowed.");
+ return BadRequest(new OpenIddictResponse
+ {
+ Error = "Not allowed", ErrorDescription = "Member authorization is not allowed."
+ });
}
- HttpContext context = _httpContextAccessor.GetRequiredHttpContext();
- OpenIddictRequest? request = context.GetOpenIddictServerRequest();
+ OpenIddictRequest? request = HttpContext.GetOpenIddictServerRequest();
if (request is null)
{
- return BadRequest("Unable to obtain OpenID data from the current request.");
+ return BadRequest(new OpenIddictResponse
+ {
+ Error = "No context found", ErrorDescription = "Unable to obtain context from the current request."
+ });
}
// make sure this endpoint ONLY handles member authentication
if (request.ClientId is not Constants.OAuthClientIds.Member)
{
- return BadRequest("The specified client ID cannot be used here.");
+ return BadRequest(new OpenIddictResponse
+ {
+ Error = "Invalid 'client ID'", ErrorDescription = "The specified 'client_id' is not valid."
+ });
}
return request.IdentityProvider.IsNullOrWhiteSpace()
@@ -75,6 +108,50 @@ public class MemberController : DeliveryApiControllerBase
: await AuthorizeExternal(request);
}
+ [HttpPost("token")]
+ [MapToApiVersion("1.0")]
+ public async Task Token()
+ {
+ OpenIddictRequest? request = HttpContext.GetOpenIddictServerRequest();
+ if (request is null)
+ {
+ return BadRequest(new OpenIddictResponse
+ {
+ Error = "No context found", ErrorDescription = "Unable to obtain context from the current request."
+ });
+ }
+
+ // authorization code flow or refresh token flow?
+ if ((request.IsAuthorizationCodeGrantType() || request.IsRefreshTokenGrantType()) && _deliveryApiSettings.MemberAuthorization?.AuthorizationCodeFlow?.Enabled is true)
+ {
+ // attempt to authorize against the supplied the authorization code
+ AuthenticateResult authenticateResult = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
+
+ return authenticateResult is { Succeeded: true, Principal: not null }
+ ? new SignInResult(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, authenticateResult.Principal)
+ : BadRequest(new OpenIddictResponse
+ {
+ Error = "Authorization failed", ErrorDescription = "The supplied authorization could not be verified."
+ });
+ }
+
+ // client credentials flow?
+ if (request.IsClientCredentialsGrantType() && _deliveryApiSettings.MemberAuthorization?.ClientCredentialsFlow?.Enabled is true)
+ {
+ // if we get here, the client ID and secret are valid (verified by OpenIddict)
+
+ MemberIdentityUser? member = await _memberClientCredentialsManager.FindMemberAsync(request.ClientId!);
+ return member is not null
+ ? await SignInMember(member, request)
+ : BadRequest(new OpenIddictResponse
+ {
+ Error = "Authorization failed", ErrorDescription = "Invalid 'client_id' or client configuration."
+ });
+ }
+
+ throw new InvalidOperationException("The requested grant type is not supported.");
+ }
+
[HttpGet("signout")]
[MapToApiVersion("1.0")]
public async Task Signout()
@@ -100,7 +177,10 @@ public class MemberController : DeliveryApiControllerBase
if (member is null)
{
_logger.LogError("The member with username {userName} was successfully authorized, but could not be retrieved by the member manager", userName);
- return BadRequest("The member could not be found.");
+ return BadRequest(new OpenIddictResponse
+ {
+ Error = "Authorization failed", ErrorDescription = "The member associated with the supplied 'client_id' could not be found."
+ });
}
return await SignInMember(member, request);
@@ -128,7 +208,10 @@ public class MemberController : DeliveryApiControllerBase
if (member is null)
{
_logger.LogError("A member was successfully authorized using external authentication, but could not be retrieved by the member manager");
- return BadRequest("The member could not be found.");
+ return BadRequest(new OpenIddictResponse
+ {
+ Error = "Authorization failed", ErrorDescription = "The member associated with the supplied 'client_id' could not be found."
+ });
}
// update member authentication tokens if succeeded
diff --git a/src/Umbraco.Cms.Api.Delivery/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Cms.Api.Delivery/DependencyInjection/UmbracoBuilderExtensions.cs
index 5ecc856f3b..34f8b058e6 100644
--- a/src/Umbraco.Cms.Api.Delivery/DependencyInjection/UmbracoBuilderExtensions.cs
+++ b/src/Umbraco.Cms.Api.Delivery/DependencyInjection/UmbracoBuilderExtensions.cs
@@ -21,6 +21,7 @@ using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Notifications;
+using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Infrastructure.Security;
using Umbraco.Cms.Web.Common.ApplicationBuilder;
@@ -60,6 +61,7 @@ public static class UmbracoBuilderExtensions
builder.Services.AddSingleton();
builder.Services.AddTransient();
builder.Services.AddTransient();
+ builder.Services.AddScoped();
builder.Services.ConfigureOptions();
builder.AddUmbracoApiOpenApiUI();
diff --git a/src/Umbraco.Cms.Api.Delivery/Handlers/InitializeMemberApplicationNotificationHandler.cs b/src/Umbraco.Cms.Api.Delivery/Handlers/InitializeMemberApplicationNotificationHandler.cs
index 659a1ba835..43653239b3 100644
--- a/src/Umbraco.Cms.Api.Delivery/Handlers/InitializeMemberApplicationNotificationHandler.cs
+++ b/src/Umbraco.Cms.Api.Delivery/Handlers/InitializeMemberApplicationNotificationHandler.cs
@@ -5,6 +5,7 @@ using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Notifications;
+using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.Security;
@@ -16,16 +17,19 @@ internal sealed class InitializeMemberApplicationNotificationHandler : INotifica
private readonly ILogger _logger;
private readonly DeliveryApiSettings _deliveryApiSettings;
private readonly IServiceScopeFactory _serviceScopeFactory;
+ private readonly IMemberClientCredentialsManager _memberClientCredentialsManager;
public InitializeMemberApplicationNotificationHandler(
IRuntimeState runtimeState,
IOptions deliveryApiSettings,
ILogger logger,
- IServiceScopeFactory serviceScopeFactory)
+ IServiceScopeFactory serviceScopeFactory,
+ IMemberClientCredentialsManager memberClientCredentialsManager)
{
_runtimeState = runtimeState;
_logger = logger;
_serviceScopeFactory = serviceScopeFactory;
+ _memberClientCredentialsManager = memberClientCredentialsManager;
_deliveryApiSettings = deliveryApiSettings.Value;
}
@@ -41,6 +45,12 @@ internal sealed class InitializeMemberApplicationNotificationHandler : INotifica
using IServiceScope scope = _serviceScopeFactory.CreateScope();
IMemberApplicationManager memberApplicationManager = scope.ServiceProvider.GetRequiredService();
+ await HandleMemberApplication(memberApplicationManager, cancellationToken);
+ await HandleMemberClientCredentialsApplication(memberApplicationManager, cancellationToken);
+ }
+
+ private async Task HandleMemberApplication(IMemberApplicationManager memberApplicationManager, CancellationToken cancellationToken)
+ {
if (_deliveryApiSettings.MemberAuthorization?.AuthorizationCodeFlow?.Enabled is not true)
{
await memberApplicationManager.DeleteMemberApplicationAsync(cancellationToken);
@@ -66,6 +76,21 @@ internal sealed class InitializeMemberApplicationNotificationHandler : INotifica
cancellationToken);
}
+ private async Task HandleMemberClientCredentialsApplication(IMemberApplicationManager memberApplicationManager, CancellationToken cancellationToken)
+ {
+ if (_deliveryApiSettings.MemberAuthorization?.ClientCredentialsFlow?.Enabled is not true)
+ {
+ // disabled
+ return;
+ }
+
+ IEnumerable memberClientCredentials = await _memberClientCredentialsManager.GetAllAsync();
+ foreach (MemberClientCredentials memberClientCredential in memberClientCredentials)
+ {
+ await memberApplicationManager.EnsureMemberClientCredentialsApplicationAsync(memberClientCredential.ClientId, memberClientCredential.ClientSecret, cancellationToken);
+ }
+ }
+
private bool ValidateRedirectUrls(Uri[] redirectUrls)
{
if (redirectUrls.Any() is false)
diff --git a/src/Umbraco.Cms.Api.Delivery/Security/MemberApplicationManager.cs b/src/Umbraco.Cms.Api.Delivery/Security/MemberApplicationManager.cs
index 92ca409194..67cfb4b7cf 100644
--- a/src/Umbraco.Cms.Api.Delivery/Security/MemberApplicationManager.cs
+++ b/src/Umbraco.Cms.Api.Delivery/Security/MemberApplicationManager.cs
@@ -64,4 +64,26 @@ public class MemberApplicationManager : OpenIdDictApplicationManagerBase, IMembe
public async Task DeleteMemberApplicationAsync(CancellationToken cancellationToken = default)
=> await Delete(Constants.OAuthClientIds.Member, cancellationToken);
+
+ public async Task EnsureMemberClientCredentialsApplicationAsync(string clientId, string clientSecret, CancellationToken cancellationToken = default)
+ {
+ var applicationDescriptor = new OpenIddictApplicationDescriptor
+ {
+ DisplayName = $"Umbraco client credentials member access: {clientId}",
+ ClientId = clientId,
+ ClientSecret = clientSecret,
+ ClientType = OpenIddictConstants.ClientTypes.Confidential,
+ Permissions =
+ {
+ OpenIddictConstants.Permissions.Endpoints.Token,
+ OpenIddictConstants.Permissions.Endpoints.Revocation,
+ OpenIddictConstants.Permissions.GrantTypes.ClientCredentials
+ }
+ };
+
+ await CreateOrUpdate(applicationDescriptor, cancellationToken);
+ }
+
+ public async Task DeleteMemberClientCredentialsApplicationAsync(string clientId, CancellationToken cancellationToken = default)
+ => await Delete(clientId, cancellationToken);
}
diff --git a/src/Umbraco.Cms.Api.Delivery/Services/ApiMediaQueryService.cs b/src/Umbraco.Cms.Api.Delivery/Services/ApiMediaQueryService.cs
index 5969e0a788..8a078d7f0d 100644
--- a/src/Umbraco.Cms.Api.Delivery/Services/ApiMediaQueryService.cs
+++ b/src/Umbraco.Cms.Api.Delivery/Services/ApiMediaQueryService.cs
@@ -85,7 +85,7 @@ internal sealed class ApiMediaQueryService : IApiMediaQueryService
return null;
}
- var childrenOf = fetch.TrimStart(childrenOfParameter);
+ var childrenOf = fetch.TrimStartExact(childrenOfParameter);
if (childrenOf.IsNullOrWhiteSpace())
{
// this mirrors the current behavior of the Content Delivery API :-)
diff --git a/src/Umbraco.Cms.Api.Delivery/Services/RequestRedirectService.cs b/src/Umbraco.Cms.Api.Delivery/Services/RequestRedirectService.cs
index 882525c8d0..4b9efc03a5 100644
--- a/src/Umbraco.Cms.Api.Delivery/Services/RequestRedirectService.cs
+++ b/src/Umbraco.Cms.Api.Delivery/Services/RequestRedirectService.cs
@@ -66,7 +66,7 @@ internal sealed class RequestRedirectService : RoutingServiceBase, IRequestRedir
}
// important: redirect URLs are always tracked without trailing slashes
- IRedirectUrl? redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl(requestedPath.TrimEnd("/"), culture);
+ IRedirectUrl? redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl(requestedPath.TrimEndExact("/"), culture);
IPublishedContent? content = redirectUrl != null
? _apiPublishedContentCache.GetById(redirectUrl.ContentKey)
: null;
diff --git a/src/Umbraco.Cms.Api.Delivery/Umbraco.Cms.Api.Delivery.csproj b/src/Umbraco.Cms.Api.Delivery/Umbraco.Cms.Api.Delivery.csproj
index 4479bc92e2..3e0075acfd 100644
--- a/src/Umbraco.Cms.Api.Delivery/Umbraco.Cms.Api.Delivery.csproj
+++ b/src/Umbraco.Cms.Api.Delivery/Umbraco.Cms.Api.Delivery.csproj
@@ -13,6 +13,10 @@
+
+
+ false
+
<_Parameter1>Umbraco.Tests.UnitTests
diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Content/ContentCollectionControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Content/ContentCollectionControllerBase.cs
index 95f4bb5498..8012da4669 100644
--- a/src/Umbraco.Cms.Api.Management/Controllers/Content/ContentCollectionControllerBase.cs
+++ b/src/Umbraco.Cms.Api.Management/Controllers/Content/ContentCollectionControllerBase.cs
@@ -14,7 +14,7 @@ namespace Umbraco.Cms.Api.Management.Controllers.Content;
public abstract class ContentCollectionControllerBase : ManagementApiControllerBase
where TContent : class, IContentBase
where TCollectionResponseModel : ContentResponseModelBase
- where TValueResponseModelBase : ValueModelBase
+ where TValueResponseModelBase : ValueResponseModelBase
where TVariantResponseModel : VariantResponseModelBase
{
private readonly IUmbracoMapper _mapper;
diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/Collection/DocumentCollectionControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/Collection/DocumentCollectionControllerBase.cs
index 71dfc75fb1..b4ac5a86da 100644
--- a/src/Umbraco.Cms.Api.Management/Controllers/Document/Collection/DocumentCollectionControllerBase.cs
+++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/Collection/DocumentCollectionControllerBase.cs
@@ -15,7 +15,7 @@ namespace Umbraco.Cms.Api.Management.Controllers.Document.Collection;
[VersionedApiBackOfficeRoute($"{Constants.Web.RoutePath.Collection}/{Constants.UdiEntityType.Document}")]
[ApiExplorerSettings(GroupName = nameof(Constants.UdiEntityType.Document))]
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocuments)]
-public abstract class DocumentCollectionControllerBase : ContentCollectionControllerBase
+public abstract class DocumentCollectionControllerBase : ContentCollectionControllerBase
{
protected DocumentCollectionControllerBase(IUmbracoMapper mapper)
: base(mapper)
diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/ValidateUpdateDocumentController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/ValidateUpdateDocumentController.cs
index f80203d135..05f029f582 100644
--- a/src/Umbraco.Cms.Api.Management/Controllers/Document/ValidateUpdateDocumentController.cs
+++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/ValidateUpdateDocumentController.cs
@@ -12,6 +12,7 @@ using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Management.Controllers.Document;
[ApiVersion("1.0")]
+[ApiVersion("1.1")]
public class ValidateUpdateDocumentController : UpdateDocumentControllerBase
{
private readonly IContentEditingService _contentEditingService;
@@ -32,10 +33,35 @@ public class ValidateUpdateDocumentController : UpdateDocumentControllerBase
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
+ [Obsolete("Please use version 1.1 of this API. Will be removed in V16.")]
public async Task Validate(CancellationToken cancellationToken, Guid id, UpdateDocumentRequestModel requestModel)
=> await HandleRequest(id, requestModel, async () =>
{
- ContentUpdateModel model = _documentEditingPresentationFactory.MapUpdateModel(requestModel);
+ var validateUpdateDocumentRequestModel = new ValidateUpdateDocumentRequestModel
+ {
+ Values = requestModel.Values,
+ Variants = requestModel.Variants,
+ Template = requestModel.Template,
+ Cultures = null
+ };
+
+ ValidateContentUpdateModel model = _documentEditingPresentationFactory.MapValidateUpdateModel(validateUpdateDocumentRequestModel);
+ Attempt result = await _contentEditingService.ValidateUpdateAsync(id, model);
+
+ return result.Success
+ ? Ok()
+ : DocumentEditingOperationStatusResult(result.Status, requestModel, result.Result);
+ });
+
+ [HttpPut("{id:guid}/validate")]
+ [MapToApiVersion("1.1")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
+ public async Task ValidateV1_1(CancellationToken cancellationToken, Guid id, ValidateUpdateDocumentRequestModel requestModel)
+ => await HandleRequest(id, requestModel, async () =>
+ {
+ ValidateContentUpdateModel model = _documentEditingPresentationFactory.MapValidateUpdateModel(requestModel);
Attempt result = await _contentEditingService.ValidateUpdateAsync(id, model);
return result.Success
diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Indexer/AllIndexerController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Indexer/AllIndexerController.cs
index 768389b56d..6ce4d3b12b 100644
--- a/src/Umbraco.Cms.Api.Management/Controllers/Indexer/AllIndexerController.cs
+++ b/src/Umbraco.Cms.Api.Management/Controllers/Indexer/AllIndexerController.cs
@@ -37,7 +37,7 @@ public class AllIndexerController : IndexerControllerBase
{
IndexResponseModel[] indexes = _examineManager.Indexes
.Select(_indexPresentationFactory.Create)
- .OrderBy(indexModel => indexModel.Name.TrimEnd("Indexer")).ToArray();
+ .OrderBy(indexModel => indexModel.Name.TrimEndExact("Indexer")).ToArray();
var viewModel = new PagedViewModel { Items = indexes.Skip(skip).Take(take), Total = indexes.Length };
return Task.FromResult(viewModel);
diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Media/Collection/MediaCollectionControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Media/Collection/MediaCollectionControllerBase.cs
index c6bf7eadda..cf6b05d8b9 100644
--- a/src/Umbraco.Cms.Api.Management/Controllers/Media/Collection/MediaCollectionControllerBase.cs
+++ b/src/Umbraco.Cms.Api.Management/Controllers/Media/Collection/MediaCollectionControllerBase.cs
@@ -15,7 +15,7 @@ namespace Umbraco.Cms.Api.Management.Controllers.Media.Collection;
[VersionedApiBackOfficeRoute($"{Constants.Web.RoutePath.Collection}/{Constants.UdiEntityType.Media}")]
[ApiExplorerSettings(GroupName = nameof(Constants.UdiEntityType.Media))]
[Authorize(Policy = AuthorizationPolicies.SectionAccessMedia)]
-public abstract class MediaCollectionControllerBase : ContentCollectionControllerBase
+public abstract class MediaCollectionControllerBase : ContentCollectionControllerBase
{
protected MediaCollectionControllerBase(IUmbracoMapper mapper)
: base(mapper)
diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Searcher/AllSearcherController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Searcher/AllSearcherController.cs
index 76cd2f639b..4c5189dcaa 100644
--- a/src/Umbraco.Cms.Api.Management/Controllers/Searcher/AllSearcherController.cs
+++ b/src/Umbraco.Cms.Api.Management/Controllers/Searcher/AllSearcherController.cs
@@ -30,7 +30,7 @@ public class AllSearcherController : SearcherControllerBase
var searchers = new List(
_examineManager.RegisteredSearchers.Select(searcher => new SearcherResponse { Name = searcher.Name })
.OrderBy(x =>
- x.Name.TrimEnd("Searcher"))); // order by name , but strip the "Searcher" from the end if it exists
+ x.Name.TrimEndExact("Searcher"))); // order by name , but strip the "Searcher" from the end if it exists
var viewModel = new PagedViewModel
{
Items = searchers.Skip(skip).Take(take),
diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Security/BackOfficeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Security/BackOfficeController.cs
index 03504a5099..fd6f67927c 100644
--- a/src/Umbraco.Cms.Api.Management/Controllers/Security/BackOfficeController.cs
+++ b/src/Umbraco.Cms.Api.Management/Controllers/Security/BackOfficeController.cs
@@ -42,6 +42,7 @@ public class BackOfficeController : SecurityControllerBase
private readonly IBackOfficeTwoFactorOptions _backOfficeTwoFactorOptions;
private readonly IUserTwoFactorLoginService _userTwoFactorLoginService;
private readonly IBackOfficeExternalLoginService _externalLoginService;
+ private readonly IBackOfficeUserClientCredentialsManager _backOfficeUserClientCredentialsManager;
private const string RedirectFlowParameter = "flow";
private const string RedirectStatusParameter = "status";
@@ -55,7 +56,8 @@ public class BackOfficeController : SecurityControllerBase
ILogger logger,
IBackOfficeTwoFactorOptions backOfficeTwoFactorOptions,
IUserTwoFactorLoginService userTwoFactorLoginService,
- IBackOfficeExternalLoginService externalLoginService)
+ IBackOfficeExternalLoginService externalLoginService,
+ IBackOfficeUserClientCredentialsManager backOfficeUserClientCredentialsManager)
{
_httpContextAccessor = httpContextAccessor;
_backOfficeSignInManager = backOfficeSignInManager;
@@ -65,6 +67,7 @@ public class BackOfficeController : SecurityControllerBase
_backOfficeTwoFactorOptions = backOfficeTwoFactorOptions;
_userTwoFactorLoginService = userTwoFactorLoginService;
_externalLoginService = externalLoginService;
+ _backOfficeUserClientCredentialsManager = backOfficeUserClientCredentialsManager;
}
[HttpPost("login")]
@@ -166,13 +169,19 @@ public class BackOfficeController : SecurityControllerBase
OpenIddictRequest? request = context.GetOpenIddictServerRequest();
if (request == null)
{
- return BadRequest("Unable to obtain OpenID data from the current request");
+ return BadRequest(new OpenIddictResponse
+ {
+ Error = "No context found", ErrorDescription = "Unable to obtain context from the current request."
+ });
}
// make sure we keep member authentication away from this endpoint
if (request.ClientId is Constants.OAuthClientIds.Member)
{
- return BadRequest("The specified client ID cannot be used here.");
+ return BadRequest(new OpenIddictResponse
+ {
+ Error = "Invalid 'client ID'", ErrorDescription = "The specified 'client_id' is not valid."
+ });
}
return request.IdentityProvider.IsNullOrWhiteSpace()
@@ -180,6 +189,61 @@ public class BackOfficeController : SecurityControllerBase
: await AuthorizeExternal(request);
}
+ [AllowAnonymous]
+ [HttpPost("token")]
+ [MapToApiVersion("1.0")]
+ public async Task Token()
+ {
+ HttpContext context = _httpContextAccessor.GetRequiredHttpContext();
+ OpenIddictRequest? request = context.GetOpenIddictServerRequest();
+ if (request == null)
+ {
+ return BadRequest(new OpenIddictResponse
+ {
+ Error = "No context found", ErrorDescription = "Unable to obtain context from the current request."
+ });
+ }
+
+ if (request.IsAuthorizationCodeGrantType() || request.IsRefreshTokenGrantType())
+ {
+ // attempt to authorize against the supplied the authorization code
+ AuthenticateResult authenticateResult = await context.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
+
+ return authenticateResult is { Succeeded: true, Principal: not null }
+ ? new SignInResult(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, authenticateResult.Principal)
+ : BadRequest(new OpenIddictResponse
+ {
+ Error = "Authorization failed", ErrorDescription = "The supplied authorization could not be verified."
+ });
+ }
+
+ if (request.IsClientCredentialsGrantType())
+ {
+ // if we get here, the client ID and secret are valid (verified by OpenIddict)
+
+ // grab the user associated with the client ID
+ BackOfficeIdentityUser? associatedUser = await _backOfficeUserClientCredentialsManager.FindUserAsync(request.ClientId!);
+
+ if (associatedUser is not null)
+ {
+ // log current datetime as last login (this also ensures that the user is not flagged as inactive)
+ associatedUser.LastLoginDateUtc = DateTime.UtcNow;
+ await _backOfficeUserManager.UpdateAsync(associatedUser);
+
+ return await SignInBackOfficeUser(associatedUser, request);
+ }
+
+ // if this happens, the OpenIddict applications have somehow gone out of sync with the Umbraco users
+ _logger.LogError("The user associated with the client ID ({clientId}) could not be found", request.ClientId);
+ return BadRequest(new OpenIddictResponse
+ {
+ Error = "Authorization failed", ErrorDescription = "The user associated with the supplied 'client_id' could not be found."
+ });
+ }
+
+ throw new InvalidOperationException("The requested grant type is not supported.");
+ }
+
[AllowAnonymous]
[HttpGet("signout")]
[MapToApiVersion("1.0")]
diff --git a/src/Umbraco.Cms.Api.Management/Controllers/User/ClientCredentials/ClientCredentialsUserControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/User/ClientCredentials/ClientCredentialsUserControllerBase.cs
new file mode 100644
index 0000000000..9a30da3811
--- /dev/null
+++ b/src/Umbraco.Cms.Api.Management/Controllers/User/ClientCredentials/ClientCredentialsUserControllerBase.cs
@@ -0,0 +1,25 @@
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Umbraco.Cms.Core.Security.OperationStatus;
+
+namespace Umbraco.Cms.Api.Management.Controllers.User.ClientCredentials;
+
+[ApiExplorerSettings(GroupName = "User")]
+public abstract class ClientCredentialsUserControllerBase : UserControllerBase
+{
+ protected IActionResult BackOfficeUserClientCredentialsOperationStatusResult(BackOfficeUserClientCredentialsOperationStatus status) =>
+ OperationStatusResult(status, problemDetailsBuilder => status switch
+ {
+ BackOfficeUserClientCredentialsOperationStatus.InvalidUser => BadRequest(problemDetailsBuilder
+ .WithTitle("Invalid user")
+ .WithDetail("The specified user does not support this operation. Possibly caused by a mismatched client ID or an inapplicable user type.")
+ .Build()),
+ BackOfficeUserClientCredentialsOperationStatus.DuplicateClientId => BadRequest(problemDetailsBuilder
+ .WithTitle("Duplicate client ID")
+ .WithDetail("The specified client ID is already in use. Choose another client ID.")
+ .Build()),
+ _ => StatusCode(StatusCodes.Status500InternalServerError, problemDetailsBuilder
+ .WithTitle("Unknown client credentials operation status.")
+ .Build()),
+ });
+}
diff --git a/src/Umbraco.Cms.Api.Management/Controllers/User/ClientCredentials/CreateClientCredentialsUserController.cs b/src/Umbraco.Cms.Api.Management/Controllers/User/ClientCredentials/CreateClientCredentialsUserController.cs
new file mode 100644
index 0000000000..d9212b18cd
--- /dev/null
+++ b/src/Umbraco.Cms.Api.Management/Controllers/User/ClientCredentials/CreateClientCredentialsUserController.cs
@@ -0,0 +1,49 @@
+using Asp.Versioning;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Umbraco.Cms.Api.Management.ViewModels.User.ClientCredentials;
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Security;
+using Umbraco.Cms.Core.Security.Authorization;
+using Umbraco.Cms.Core.Security.OperationStatus;
+using Umbraco.Cms.Web.Common.Authorization;
+using Umbraco.Extensions;
+
+namespace Umbraco.Cms.Api.Management.Controllers.User.ClientCredentials;
+
+[ApiVersion("1.0")]
+public class CreateClientCredentialsUserController : ClientCredentialsUserControllerBase
+{
+ private readonly IBackOfficeUserClientCredentialsManager _backOfficeUserClientCredentialsManager;
+ private readonly IAuthorizationService _authorizationService;
+
+ public CreateClientCredentialsUserController(
+ IBackOfficeUserClientCredentialsManager backOfficeUserClientCredentialsManager,
+ IAuthorizationService authorizationService)
+ {
+ _backOfficeUserClientCredentialsManager = backOfficeUserClientCredentialsManager;
+ _authorizationService = authorizationService;
+ }
+
+ [HttpPost("{id:guid}/client-credentials")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
+ public async Task Create(CancellationToken cancellationToken, Guid id, CreateUserClientCredentialsRequestModel model)
+ {
+ AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
+ User,
+ UserPermissionResource.WithKeys(id),
+ AuthorizationPolicies.UserPermissionByResource);
+
+ if (!authorizationResult.Succeeded)
+ {
+ return Forbidden();
+ }
+
+ Attempt result = await _backOfficeUserClientCredentialsManager.SaveAsync(id, model.ClientId, model.ClientSecret);
+ return result.Success
+ ? Ok()
+ : BackOfficeUserClientCredentialsOperationStatusResult(result.Result);
+ }
+}
diff --git a/src/Umbraco.Cms.Api.Management/Controllers/User/ClientCredentials/DeleteClientCredentialsUserController.cs b/src/Umbraco.Cms.Api.Management/Controllers/User/ClientCredentials/DeleteClientCredentialsUserController.cs
new file mode 100644
index 0000000000..5ad6e4ab1e
--- /dev/null
+++ b/src/Umbraco.Cms.Api.Management/Controllers/User/ClientCredentials/DeleteClientCredentialsUserController.cs
@@ -0,0 +1,48 @@
+using Asp.Versioning;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Security;
+using Umbraco.Cms.Core.Security.Authorization;
+using Umbraco.Cms.Core.Security.OperationStatus;
+using Umbraco.Cms.Web.Common.Authorization;
+using Umbraco.Extensions;
+
+namespace Umbraco.Cms.Api.Management.Controllers.User.ClientCredentials;
+
+[ApiVersion("1.0")]
+public class DeleteClientCredentialsUserController : ClientCredentialsUserControllerBase
+{
+ private readonly IBackOfficeUserClientCredentialsManager _backOfficeUserClientCredentialsManager;
+ private readonly IAuthorizationService _authorizationService;
+
+ public DeleteClientCredentialsUserController(
+ IBackOfficeUserClientCredentialsManager backOfficeUserClientCredentialsManager,
+ IAuthorizationService authorizationService)
+ {
+ _backOfficeUserClientCredentialsManager = backOfficeUserClientCredentialsManager;
+ _authorizationService = authorizationService;
+ }
+
+ [HttpDelete("{id:guid}/client-credentials/{clientId}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
+ public async Task Delete(CancellationToken cancellationToken, Guid id, string clientId)
+ {
+ AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
+ User,
+ UserPermissionResource.WithKeys(id),
+ AuthorizationPolicies.UserPermissionByResource);
+
+ if (!authorizationResult.Succeeded)
+ {
+ return Forbidden();
+ }
+
+ Attempt result = await _backOfficeUserClientCredentialsManager.DeleteAsync(id, clientId);
+ return result.Success
+ ? Ok()
+ : BackOfficeUserClientCredentialsOperationStatusResult(result.Result);
+ }
+}
diff --git a/src/Umbraco.Cms.Api.Management/Controllers/User/ClientCredentials/GetAllClientCredentialsUserController.cs b/src/Umbraco.Cms.Api.Management/Controllers/User/ClientCredentials/GetAllClientCredentialsUserController.cs
new file mode 100644
index 0000000000..e1e4d46f66
--- /dev/null
+++ b/src/Umbraco.Cms.Api.Management/Controllers/User/ClientCredentials/GetAllClientCredentialsUserController.cs
@@ -0,0 +1,43 @@
+using Asp.Versioning;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Umbraco.Cms.Core.Security;
+using Umbraco.Cms.Core.Security.Authorization;
+using Umbraco.Cms.Web.Common.Authorization;
+using Umbraco.Extensions;
+
+namespace Umbraco.Cms.Api.Management.Controllers.User.ClientCredentials;
+
+[ApiVersion("1.0")]
+public class GetAllClientCredentialsUserController : ClientCredentialsUserControllerBase
+{
+ private readonly IBackOfficeUserClientCredentialsManager _backOfficeUserClientCredentialsManager;
+ private readonly IAuthorizationService _authorizationService;
+
+ public GetAllClientCredentialsUserController(
+ IBackOfficeUserClientCredentialsManager backOfficeUserClientCredentialsManager,
+ IAuthorizationService authorizationService)
+ {
+ _backOfficeUserClientCredentialsManager = backOfficeUserClientCredentialsManager;
+ _authorizationService = authorizationService;
+ }
+
+ [HttpGet("{id:guid}/client-credentials")]
+ [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)]
+ public async Task GetAll(CancellationToken cancellationToken, Guid id)
+ {
+ AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
+ User,
+ UserPermissionResource.WithKeys(id),
+ AuthorizationPolicies.UserPermissionByResource);
+
+ if (!authorizationResult.Succeeded)
+ {
+ return Forbidden();
+ }
+
+ IEnumerable clientIds = await _backOfficeUserClientCredentialsManager.GetClientIdsAsync(id);
+ return Ok(clientIds);
+ }
+}
diff --git a/src/Umbraco.Cms.Api.Management/Controllers/User/UserOrCurrentUserControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/User/UserOrCurrentUserControllerBase.cs
index 7e5488a20c..65490a9c21 100644
--- a/src/Umbraco.Cms.Api.Management/Controllers/User/UserOrCurrentUserControllerBase.cs
+++ b/src/Umbraco.Cms.Api.Management/Controllers/User/UserOrCurrentUserControllerBase.cs
@@ -128,6 +128,10 @@ public abstract class UserOrCurrentUserControllerBase : ManagementApiControllerB
.WithTitle("Self password reset not allowed")
.WithDetail("It is not allowed to reset the password for the account you are logged in to.")
.Build()),
+ UserOperationStatus.InvalidUserType => BadRequest(problemDetailsBuilder
+ .WithTitle("Invalid user type")
+ .WithDetail("The target user type does not support this operation.")
+ .Build()),
UserOperationStatus.Forbidden => Forbidden(),
_ => StatusCode(StatusCodes.Status500InternalServerError, problemDetailsBuilder
.WithTitle("Unknown user operation status.")
diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/MemberBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/MemberBuilderExtensions.cs
index 90ea476c18..b5a5bb842c 100644
--- a/src/Umbraco.Cms.Api.Management/DependencyInjection/MemberBuilderExtensions.cs
+++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/MemberBuilderExtensions.cs
@@ -10,7 +10,7 @@ internal static class MemberBuilderExtensions
{
internal static IUmbracoBuilder AddMember(this IUmbracoBuilder builder)
{
- builder.Services.AddTransient();
+ builder.Services.AddSingleton();
builder.Services.AddTransient();
builder.WithCollectionBuilder().Add();
diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilder.BackOffice.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilder.BackOffice.cs
index 32ae43b1d1..c2b0b4d720 100644
--- a/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilder.BackOffice.cs
+++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilder.BackOffice.cs
@@ -35,6 +35,7 @@ public static partial class UmbracoBuilderExtensions
.AddWebServer()
.AddRecurringBackgroundJobs()
.AddNuCache()
+ .AddUmbracoHybridCache()
.AddDistributedCache()
.AddCoreNotifications()
.AddExamine()
diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilder.BackOfficeIdentity.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilder.BackOfficeIdentity.cs
index baf8c2ebcb..15dd835fa0 100644
--- a/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilder.BackOfficeIdentity.cs
+++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilder.BackOfficeIdentity.cs
@@ -65,6 +65,7 @@ public static partial class UmbracoBuilderExtensions
services.AddScoped();
services.AddScoped();
services.AddScoped();
+ services.AddScoped();
services.AddSingleton();
diff --git a/src/Umbraco.Cms.Api.Management/Factories/DocumentEditingPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/DocumentEditingPresentationFactory.cs
index 4f96cdb099..31b1a9a66f 100644
--- a/src/Umbraco.Cms.Api.Management/Factories/DocumentEditingPresentationFactory.cs
+++ b/src/Umbraco.Cms.Api.Management/Factories/DocumentEditingPresentationFactory.cs
@@ -17,8 +17,20 @@ internal sealed class DocumentEditingPresentationFactory : ContentEditingPresent
}
public ContentUpdateModel MapUpdateModel(UpdateDocumentRequestModel requestModel)
+ => MapUpdateContentModel(requestModel);
+
+ public ValidateContentUpdateModel MapValidateUpdateModel(ValidateUpdateDocumentRequestModel requestModel)
{
- ContentUpdateModel model = MapContentEditingModel(requestModel);
+ ValidateContentUpdateModel model = MapUpdateContentModel(requestModel);
+ model.Cultures = requestModel.Cultures;
+
+ return model;
+ }
+
+ private TUpdateModel MapUpdateContentModel(UpdateDocumentRequestModel requestModel)
+ where TUpdateModel : ContentUpdateModel, new()
+ {
+ TUpdateModel model = MapContentEditingModel(requestModel);
model.TemplateKey = requestModel.Template?.Id;
return model;
diff --git a/src/Umbraco.Cms.Api.Management/Factories/IDocumentEditingPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/IDocumentEditingPresentationFactory.cs
index dbc8538563..52978698d4 100644
--- a/src/Umbraco.Cms.Api.Management/Factories/IDocumentEditingPresentationFactory.cs
+++ b/src/Umbraco.Cms.Api.Management/Factories/IDocumentEditingPresentationFactory.cs
@@ -8,4 +8,6 @@ public interface IDocumentEditingPresentationFactory
ContentCreateModel MapCreateModel(CreateDocumentRequestModel requestModel);
ContentUpdateModel MapUpdateModel(UpdateDocumentRequestModel requestModel);
+
+ ValidateContentUpdateModel MapValidateUpdateModel(ValidateUpdateDocumentRequestModel requestModel);
}
diff --git a/src/Umbraco.Cms.Api.Management/Factories/MemberPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/MemberPresentationFactory.cs
index 7e53676c5f..aaa4240caa 100644
--- a/src/Umbraco.Cms.Api.Management/Factories/MemberPresentationFactory.cs
+++ b/src/Umbraco.Cms.Api.Management/Factories/MemberPresentationFactory.cs
@@ -1,7 +1,9 @@
-using Umbraco.Cms.Api.Management.ViewModels.Content;
+using Microsoft.Extensions.Options;
+using Umbraco.Cms.Api.Management.ViewModels.Content;
using Umbraco.Cms.Api.Management.ViewModels.Member;
using Umbraco.Cms.Api.Management.ViewModels.Member.Item;
using Umbraco.Cms.Api.Management.ViewModels.MemberType;
+using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Extensions;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
@@ -19,19 +21,23 @@ internal sealed class MemberPresentationFactory : IMemberPresentationFactory
private readonly IMemberTypeService _memberTypeService;
private readonly ITwoFactorLoginService _twoFactorLoginService;
private readonly IMemberGroupService _memberGroupService;
+ private readonly DeliveryApiSettings _deliveryApiSettings;
+ private IEnumerable? _clientCredentialsMemberKeys;
public MemberPresentationFactory(
IUmbracoMapper umbracoMapper,
IMemberService memberService,
IMemberTypeService memberTypeService,
ITwoFactorLoginService twoFactorLoginService,
- IMemberGroupService memberGroupService)
+ IMemberGroupService memberGroupService,
+ IOptions deliveryApiSettings)
{
_umbracoMapper = umbracoMapper;
_memberService = memberService;
_memberTypeService = memberTypeService;
_twoFactorLoginService = twoFactorLoginService;
_memberGroupService = memberGroupService;
+ _deliveryApiSettings = deliveryApiSettings.Value;
}
public async Task CreateResponseModelAsync(IMember member, IUser currentUser)
@@ -39,6 +45,7 @@ internal sealed class MemberPresentationFactory : IMemberPresentationFactory
MemberResponseModel responseModel = _umbracoMapper.Map(member)!;
responseModel.IsTwoFactorEnabled = await _twoFactorLoginService.IsTwoFactorEnabledAsync(member.Key);
+ responseModel.Kind = GetMemberKind(member.Key);
IEnumerable roles = _memberService.GetAllRoles(member.Username);
// Get the member groups per role, so we can return the group keys
@@ -71,7 +78,8 @@ internal sealed class MemberPresentationFactory : IMemberPresentationFactory
{
Id = entity.Key,
MemberType = _umbracoMapper.Map(entity)!,
- Variants = CreateVariantsItemResponseModels(entity)
+ Variants = CreateVariantsItemResponseModels(entity),
+ Kind = GetMemberKind(entity.Key)
};
private static IEnumerable CreateVariantsItemResponseModels(ITreeEntity entity)
@@ -108,4 +116,24 @@ internal sealed class MemberPresentationFactory : IMemberPresentationFactory
return responseModel;
}
+
+ private MemberKind GetMemberKind(Guid key)
+ {
+ if (_clientCredentialsMemberKeys is null)
+ {
+ IEnumerable clientCredentialsMemberUserNames = _deliveryApiSettings
+ .MemberAuthorization?
+ .ClientCredentialsFlow?
+ .AssociatedMembers
+ .Select(m => m.UserName).ToArray()
+ ?? [];
+
+ _clientCredentialsMemberKeys = clientCredentialsMemberUserNames
+ .Select(_memberService.GetByUsername)
+ .WhereNotNull()
+ .Select(m => m.Key).ToArray();
+ }
+
+ return _clientCredentialsMemberKeys.Contains(key) ? MemberKind.Api : MemberKind.Default;
+ }
}
diff --git a/src/Umbraco.Cms.Api.Management/Factories/UserPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/UserPresentationFactory.cs
index 59e94b5d43..7ba1229416 100644
--- a/src/Umbraco.Cms.Api.Management/Factories/UserPresentationFactory.cs
+++ b/src/Umbraco.Cms.Api.Management/Factories/UserPresentationFactory.cs
@@ -80,6 +80,7 @@ public class UserPresentationFactory : IUserPresentationFactory
LastLockoutDate = user.LastLockoutDate,
LastPasswordChangeDate = user.LastPasswordChangeDate,
IsAdmin = user.IsAdmin(),
+ Kind = user.Kind
};
return responseModel;
@@ -92,6 +93,7 @@ public class UserPresentationFactory : IUserPresentationFactory
Name = user.Name ?? user.Username,
AvatarUrls = user.GetUserAvatarUrls(_appCaches.RuntimeCache, _mediaFileManager, _imageUrlGenerator)
.Select(url => _absoluteUrlBuilder.ToAbsoluteUrl(url).ToString()),
+ Kind = user.Kind
};
public async Task CreateCreationModelAsync(CreateUserRequestModel requestModel)
@@ -103,6 +105,7 @@ public class UserPresentationFactory : IUserPresentationFactory
Name = requestModel.Name,
UserName = requestModel.UserName,
UserGroupKeys = requestModel.UserGroupIds.Select(x => x.Id).ToHashSet(),
+ Kind = requestModel.Kind
};
return await Task.FromResult(createModel);
diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Content/ContentMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/Content/ContentMapDefinition.cs
index bba23ea98d..038e3fbf8b 100644
--- a/src/Umbraco.Cms.Api.Management/Mapping/Content/ContentMapDefinition.cs
+++ b/src/Umbraco.Cms.Api.Management/Mapping/Content/ContentMapDefinition.cs
@@ -8,7 +8,7 @@ namespace Umbraco.Cms.Api.Management.Mapping.Content;
public abstract class ContentMapDefinition
where TContent : IContentBase
- where TValueViewModel : ValueModelBase, new()
+ where TValueViewModel : ValueResponseModelBase, new()
where TVariantViewModel : VariantResponseModelBase, new()
{
private readonly PropertyEditorCollection _propertyEditorCollection;
@@ -36,7 +36,8 @@ public abstract class ContentMapDefinition, IMapDefinition
+public class DocumentMapDefinition : ContentMapDefinition, IMapDefinition
{
private readonly CommonMapper _commonMapper;
diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Document/DocumentVersionMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/Document/DocumentVersionMapDefinition.cs
index 40359b08c5..5e12e245bd 100644
--- a/src/Umbraco.Cms.Api.Management/Mapping/Document/DocumentVersionMapDefinition.cs
+++ b/src/Umbraco.Cms.Api.Management/Mapping/Document/DocumentVersionMapDefinition.cs
@@ -9,7 +9,7 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Mapping.Document;
-public class DocumentVersionMapDefinition : ContentMapDefinition, IMapDefinition
+public class DocumentVersionMapDefinition : ContentMapDefinition, IMapDefinition
{
public DocumentVersionMapDefinition(PropertyEditorCollection propertyEditorCollection)
: base(propertyEditorCollection)
diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Media/MediaMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/Media/MediaMapDefinition.cs
index d4760abe2d..df1743d235 100644
--- a/src/Umbraco.Cms.Api.Management/Mapping/Media/MediaMapDefinition.cs
+++ b/src/Umbraco.Cms.Api.Management/Mapping/Media/MediaMapDefinition.cs
@@ -10,7 +10,7 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Mapping.Media;
-public class MediaMapDefinition : ContentMapDefinition, IMapDefinition
+public class MediaMapDefinition : ContentMapDefinition, IMapDefinition
{
private readonly CommonMapper _commonMapper;
diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Member/MemberMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/Member/MemberMapDefinition.cs
index 2310da80fa..fedc8e4eca 100644
--- a/src/Umbraco.Cms.Api.Management/Mapping/Member/MemberMapDefinition.cs
+++ b/src/Umbraco.Cms.Api.Management/Mapping/Member/MemberMapDefinition.cs
@@ -7,7 +7,7 @@ using Umbraco.Cms.Core.PropertyEditors;
namespace Umbraco.Cms.Api.Management.Mapping.Member;
-public class MemberMapDefinition : ContentMapDefinition, IMapDefinition
+public class MemberMapDefinition : ContentMapDefinition, IMapDefinition
{
public MemberMapDefinition(PropertyEditorCollection propertyEditorCollection)
: base(propertyEditorCollection)
@@ -17,7 +17,7 @@ public class MemberMapDefinition : ContentMapDefinition mapper.Define((_, _) => new MemberResponseModel(), Map);
- // Umbraco.Code.MapAll -IsTwoFactorEnabled -Groups
+ // Umbraco.Code.MapAll -IsTwoFactorEnabled -Groups -Kind
private void Map(IMember source, MemberResponseModel target, MapperContext context)
{
target.Id = source.Key;
diff --git a/src/Umbraco.Cms.Api.Management/Mapping/PartialView/PartialViewViewModelsMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/PartialView/PartialViewViewModelsMapDefinition.cs
index 97e241feff..53c1efeb3f 100644
--- a/src/Umbraco.Cms.Api.Management/Mapping/PartialView/PartialViewViewModelsMapDefinition.cs
+++ b/src/Umbraco.Cms.Api.Management/Mapping/PartialView/PartialViewViewModelsMapDefinition.cs
@@ -64,7 +64,7 @@ public class PartialViewViewModelsMapDefinition : IMapDefinition
{
target.Path = source.Path.SystemPathToVirtualPath();
target.Name = source.Name;
- target.Parent = source.ParentPath.IsNullOrEmpty()
+ target.Parent = string.IsNullOrEmpty(source.ParentPath)
? null
: new FileSystemFolderModel
{
diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Script/ScriptViewModelsMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/Script/ScriptViewModelsMapDefinition.cs
index 75096091c3..dae8bcf177 100644
--- a/src/Umbraco.Cms.Api.Management/Mapping/Script/ScriptViewModelsMapDefinition.cs
+++ b/src/Umbraco.Cms.Api.Management/Mapping/Script/ScriptViewModelsMapDefinition.cs
@@ -59,7 +59,7 @@ public class ScriptViewModelsMapDefinition : IMapDefinition
{
target.Path = source.Path.SystemPathToVirtualPath();
target.Name = source.Name;
- target.Parent = source.ParentPath.IsNullOrEmpty()
+ target.Parent = string.IsNullOrEmpty(source.ParentPath)
? null
: new FileSystemFolderModel
{
diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Stylesheet/StylesheetviewModelsMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/Stylesheet/StylesheetviewModelsMapDefinition.cs
index 30aa1651ec..65c1fe0be9 100644
--- a/src/Umbraco.Cms.Api.Management/Mapping/Stylesheet/StylesheetviewModelsMapDefinition.cs
+++ b/src/Umbraco.Cms.Api.Management/Mapping/Stylesheet/StylesheetviewModelsMapDefinition.cs
@@ -59,7 +59,7 @@ public class StylesheetViewModelsMapDefinition : IMapDefinition
{
target.Path = source.Path.SystemPathToVirtualPath();
target.Name = source.Name;
- target.Parent = source.ParentPath.IsNullOrEmpty()
+ target.Parent = string.IsNullOrEmpty(source.ParentPath)
? null
: new FileSystemFolderModel
{
diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json
index 6482df8801..29acbdd16a 100644
--- a/src/Umbraco.Cms.Api.Management/OpenApi.json
+++ b/src/Umbraco.Cms.Api.Management/OpenApi.json
@@ -9372,6 +9372,149 @@
}
}
},
+ "deprecated": true,
+ "security": [
+ {
+ "Backoffice User": [ ]
+ }
+ ]
+ }
+ },
+ "/umbraco/management/api/v1.1/document/{id}/validate": {
+ "put": {
+ "tags": [
+ "Document"
+ ],
+ "operationId": "PutUmbracoManagementApiV1.1DocumentByIdValidate1.1",
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "format": "uuid"
+ }
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/ValidateUpdateDocumentRequestModel"
+ }
+ ]
+ }
+ },
+ "text/json": {
+ "schema": {
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/ValidateUpdateDocumentRequestModel"
+ }
+ ]
+ }
+ },
+ "application/*+json": {
+ "schema": {
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/ValidateUpdateDocumentRequestModel"
+ }
+ ]
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK",
+ "headers": {
+ "Umb-Notifications": {
+ "description": "The list of notifications produced during the request.",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/NotificationHeaderModel"
+ },
+ "nullable": true
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "headers": {
+ "Umb-Notifications": {
+ "description": "The list of notifications produced during the request.",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/NotificationHeaderModel"
+ },
+ "nullable": true
+ }
+ }
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ ]
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not Found",
+ "headers": {
+ "Umb-Notifications": {
+ "description": "The list of notifications produced during the request.",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/NotificationHeaderModel"
+ },
+ "nullable": true
+ }
+ }
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ ]
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "The resource is protected and requires an authentication token"
+ },
+ "403": {
+ "description": "The authenticated user do not have access to this resource",
+ "headers": {
+ "Umb-Notifications": {
+ "description": "The list of notifications produced during the request.",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/NotificationHeaderModel"
+ },
+ "nullable": true
+ }
+ }
+ }
+ }
+ },
"security": [
{
"Backoffice User": [ ]
@@ -30621,6 +30764,258 @@
]
}
},
+ "/umbraco/management/api/v1/user/{id}/client-credentials": {
+ "post": {
+ "tags": [
+ "User"
+ ],
+ "operationId": "PostUserByIdClientCredentials",
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "format": "uuid"
+ }
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/CreateUserClientCredentialsRequestModel"
+ }
+ ]
+ }
+ },
+ "text/json": {
+ "schema": {
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/CreateUserClientCredentialsRequestModel"
+ }
+ ]
+ }
+ },
+ "application/*+json": {
+ "schema": {
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/CreateUserClientCredentialsRequestModel"
+ }
+ ]
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK",
+ "headers": {
+ "Umb-Notifications": {
+ "description": "The list of notifications produced during the request.",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/NotificationHeaderModel"
+ },
+ "nullable": true
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "headers": {
+ "Umb-Notifications": {
+ "description": "The list of notifications produced during the request.",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/NotificationHeaderModel"
+ },
+ "nullable": true
+ }
+ }
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ ]
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "The resource is protected and requires an authentication token"
+ },
+ "403": {
+ "description": "The authenticated user do not have access to this resource",
+ "headers": {
+ "Umb-Notifications": {
+ "description": "The list of notifications produced during the request.",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/NotificationHeaderModel"
+ },
+ "nullable": true
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "Backoffice User": [ ]
+ }
+ ]
+ },
+ "get": {
+ "tags": [
+ "User"
+ ],
+ "operationId": "GetUserByIdClientCredentials",
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "format": "uuid"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "The resource is protected and requires an authentication token"
+ },
+ "403": {
+ "description": "The authenticated user do not have access to this resource"
+ }
+ },
+ "security": [
+ {
+ "Backoffice User": [ ]
+ }
+ ]
+ }
+ },
+ "/umbraco/management/api/v1/user/{id}/client-credentials/{clientId}": {
+ "delete": {
+ "tags": [
+ "User"
+ ],
+ "operationId": "DeleteUserByIdClientCredentialsByClientId",
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "format": "uuid"
+ }
+ },
+ {
+ "name": "clientId",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "headers": {
+ "Umb-Notifications": {
+ "description": "The list of notifications produced during the request.",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/NotificationHeaderModel"
+ },
+ "nullable": true
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "headers": {
+ "Umb-Notifications": {
+ "description": "The list of notifications produced during the request.",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/NotificationHeaderModel"
+ },
+ "nullable": true
+ }
+ }
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ ]
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "The resource is protected and requires an authentication token"
+ },
+ "403": {
+ "description": "The authenticated user do not have access to this resource",
+ "headers": {
+ "Umb-Notifications": {
+ "description": "The list of notifications produced during the request.",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/NotificationHeaderModel"
+ },
+ "nullable": true
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "Backoffice User": [ ]
+ }
+ ]
+ }
+ },
"/umbraco/management/api/v1/user/{id}/reset-password": {
"post": {
"tags": [
@@ -35158,6 +35553,22 @@
},
"additionalProperties": false
},
+ "CreateUserClientCredentialsRequestModel": {
+ "required": [
+ "clientId",
+ "clientSecret"
+ ],
+ "type": "object",
+ "properties": {
+ "clientId": {
+ "type": "string"
+ },
+ "clientSecret": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
"CreateUserDataRequestModel": {
"required": [
"group",
@@ -35276,6 +35687,7 @@
"CreateUserRequestModel": {
"required": [
"email",
+ "kind",
"name",
"userGroupIds",
"userName"
@@ -35306,6 +35718,9 @@
"type": "string",
"format": "uuid",
"nullable": true
+ },
+ "kind": {
+ "$ref": "#/components/schemas/UserKindModel"
}
},
"additionalProperties": false
@@ -36093,7 +36508,7 @@
"items": {
"oneOf": [
{
- "$ref": "#/components/schemas/DocumentValueModel"
+ "$ref": "#/components/schemas/DocumentValueResponseModel"
}
]
}
@@ -36178,7 +36593,7 @@
"items": {
"oneOf": [
{
- "$ref": "#/components/schemas/DocumentValueModel"
+ "$ref": "#/components/schemas/DocumentValueResponseModel"
}
]
}
@@ -36435,7 +36850,7 @@
"items": {
"oneOf": [
{
- "$ref": "#/components/schemas/DocumentValueModel"
+ "$ref": "#/components/schemas/DocumentValueResponseModel"
}
]
}
@@ -37117,6 +37532,35 @@
},
"additionalProperties": false
},
+ "DocumentValueResponseModel": {
+ "required": [
+ "alias",
+ "editorAlias"
+ ],
+ "type": "object",
+ "properties": {
+ "culture": {
+ "type": "string",
+ "nullable": true
+ },
+ "segment": {
+ "type": "string",
+ "nullable": true
+ },
+ "alias": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "value": {
+ "nullable": true
+ },
+ "editorAlias": {
+ "minLength": 1,
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
"DocumentVariantItemResponseModel": {
"required": [
"name",
@@ -37275,7 +37719,7 @@
"items": {
"oneOf": [
{
- "$ref": "#/components/schemas/DocumentValueModel"
+ "$ref": "#/components/schemas/DocumentValueResponseModel"
}
]
}
@@ -38319,7 +38763,7 @@
"items": {
"oneOf": [
{
- "$ref": "#/components/schemas/MediaValueModel"
+ "$ref": "#/components/schemas/MediaValueResponseModel"
}
]
}
@@ -38502,7 +38946,7 @@
"items": {
"oneOf": [
{
- "$ref": "#/components/schemas/MediaValueModel"
+ "$ref": "#/components/schemas/MediaValueResponseModel"
}
]
}
@@ -39090,6 +39534,35 @@
},
"additionalProperties": false
},
+ "MediaValueResponseModel": {
+ "required": [
+ "alias",
+ "editorAlias"
+ ],
+ "type": "object",
+ "properties": {
+ "culture": {
+ "type": "string",
+ "nullable": true
+ },
+ "segment": {
+ "type": "string",
+ "nullable": true
+ },
+ "alias": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "value": {
+ "nullable": true
+ },
+ "editorAlias": {
+ "minLength": 1,
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
"MediaVariantRequestModel": {
"required": [
"name"
@@ -39196,6 +39669,7 @@
"MemberItemResponseModel": {
"required": [
"id",
+ "kind",
"memberType",
"variants"
],
@@ -39221,10 +39695,20 @@
}
]
}
+ },
+ "kind": {
+ "$ref": "#/components/schemas/MemberKindModel"
}
},
"additionalProperties": false
},
+ "MemberKindModel": {
+ "enum": [
+ "Default",
+ "Api"
+ ],
+ "type": "string"
+ },
"MemberResponseModel": {
"required": [
"email",
@@ -39234,6 +39718,7 @@
"isApproved",
"isLockedOut",
"isTwoFactorEnabled",
+ "kind",
"memberType",
"username",
"values",
@@ -39246,7 +39731,7 @@
"items": {
"oneOf": [
{
- "$ref": "#/components/schemas/MemberValueModel"
+ "$ref": "#/components/schemas/MemberValueResponseModel"
}
]
}
@@ -39312,6 +39797,9 @@
"type": "string",
"format": "uuid"
}
+ },
+ "kind": {
+ "$ref": "#/components/schemas/MemberKindModel"
}
},
"additionalProperties": false
@@ -39728,6 +40216,35 @@
},
"additionalProperties": false
},
+ "MemberValueResponseModel": {
+ "required": [
+ "alias",
+ "editorAlias"
+ ],
+ "type": "object",
+ "properties": {
+ "culture": {
+ "type": "string",
+ "nullable": true
+ },
+ "segment": {
+ "type": "string",
+ "nullable": true
+ },
+ "alias": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "value": {
+ "nullable": true
+ },
+ "editorAlias": {
+ "minLength": 1,
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
"MemberVariantRequestModel": {
"required": [
"name"
@@ -44888,6 +45405,7 @@
"required": [
"avatarUrls",
"id",
+ "kind",
"name"
],
"type": "object",
@@ -44904,10 +45422,20 @@
"items": {
"type": "string"
}
+ },
+ "kind": {
+ "$ref": "#/components/schemas/UserKindModel"
}
},
"additionalProperties": false
},
+ "UserKindModel": {
+ "enum": [
+ "Default",
+ "Api"
+ ],
+ "type": "string"
+ },
"UserOrderModel": {
"enum": [
"UserName",
@@ -44974,6 +45502,7 @@
"hasMediaRootAccess",
"id",
"isAdmin",
+ "kind",
"mediaStartNodeIds",
"name",
"state",
@@ -45077,6 +45606,9 @@
},
"isAdmin": {
"type": "boolean"
+ },
+ "kind": {
+ "$ref": "#/components/schemas/UserKindModel"
}
},
"additionalProperties": false
@@ -45137,6 +45669,52 @@
},
"additionalProperties": false
},
+ "ValidateUpdateDocumentRequestModel": {
+ "required": [
+ "values",
+ "variants"
+ ],
+ "type": "object",
+ "properties": {
+ "values": {
+ "type": "array",
+ "items": {
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/DocumentValueModel"
+ }
+ ]
+ }
+ },
+ "variants": {
+ "type": "array",
+ "items": {
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/DocumentVariantRequestModel"
+ }
+ ]
+ }
+ },
+ "template": {
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/ReferenceByIdModel"
+ }
+ ],
+ "nullable": true
+ },
+ "cultures": {
+ "uniqueItems": true,
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "nullable": true
+ }
+ },
+ "additionalProperties": false
+ },
"VariantItemResponseModel": {
"required": [
"name"
diff --git a/src/Umbraco.Cms.Api.Management/Security/BackOfficeApplicationManager.cs b/src/Umbraco.Cms.Api.Management/Security/BackOfficeApplicationManager.cs
index d8779e6299..36fe1b0acc 100644
--- a/src/Umbraco.Cms.Api.Management/Security/BackOfficeApplicationManager.cs
+++ b/src/Umbraco.Cms.Api.Management/Security/BackOfficeApplicationManager.cs
@@ -141,5 +141,27 @@ public class BackOfficeApplicationManager : OpenIdDictApplicationManagerBase, IB
};
}
+ public async Task EnsureBackOfficeClientCredentialsApplicationAsync(string clientId, string clientSecret, CancellationToken cancellationToken = default)
+ {
+ var applicationDescriptor = new OpenIddictApplicationDescriptor
+ {
+ DisplayName = $"Umbraco client credentials back-office access: {clientId}",
+ ClientId = clientId,
+ ClientSecret = clientSecret,
+ ClientType = OpenIddictConstants.ClientTypes.Confidential,
+ Permissions =
+ {
+ OpenIddictConstants.Permissions.Endpoints.Token,
+ OpenIddictConstants.Permissions.Endpoints.Revocation,
+ OpenIddictConstants.Permissions.GrantTypes.ClientCredentials
+ }
+ };
+
+ await CreateOrUpdate(applicationDescriptor, cancellationToken);
+ }
+
+ public async Task DeleteBackOfficeClientCredentialsApplicationAsync(string clientId, CancellationToken cancellationToken = default)
+ => await Delete(clientId, cancellationToken);
+
private static Uri CallbackUrlFor(Uri url, string relativePath) => new Uri($"{url.GetLeftPart(UriPartial.Authority)}/{relativePath.TrimStart(Constants.CharArrays.ForwardSlash)}");
}
diff --git a/src/Umbraco.Cms.Api.Management/Umbraco.Cms.Api.Management.csproj b/src/Umbraco.Cms.Api.Management/Umbraco.Cms.Api.Management.csproj
index e2060938c6..2664abe675 100644
--- a/src/Umbraco.Cms.Api.Management/Umbraco.Cms.Api.Management.csproj
+++ b/src/Umbraco.Cms.Api.Management/Umbraco.Cms.Api.Management.csproj
@@ -24,6 +24,7 @@
+
diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Content/ContentCollectionResponseModelBase.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Content/ContentCollectionResponseModelBase.cs
index 735e448fb2..2426abbecd 100644
--- a/src/Umbraco.Cms.Api.Management/ViewModels/Content/ContentCollectionResponseModelBase.cs
+++ b/src/Umbraco.Cms.Api.Management/ViewModels/Content/ContentCollectionResponseModelBase.cs
@@ -4,7 +4,7 @@ namespace Umbraco.Cms.Api.Management.ViewModels.Content;
public abstract class ContentCollectionResponseModelBase
: ContentResponseModelBase
- where TValueResponseModelBase : ValueModelBase
+ where TValueResponseModelBase : ValueResponseModelBase
where TVariantResponseModel : VariantResponseModelBase
{
public string? Creator { get; set; }
diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Document/Collection/DocumentCollectionResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Document/Collection/DocumentCollectionResponseModel.cs
index 453f2ee72a..b67f4088ce 100644
--- a/src/Umbraco.Cms.Api.Management/ViewModels/Document/Collection/DocumentCollectionResponseModel.cs
+++ b/src/Umbraco.Cms.Api.Management/ViewModels/Document/Collection/DocumentCollectionResponseModel.cs
@@ -3,7 +3,7 @@ using Umbraco.Cms.Api.Management.ViewModels.DocumentType;
namespace Umbraco.Cms.Api.Management.ViewModels.Document.Collection;
-public class DocumentCollectionResponseModel : ContentCollectionResponseModelBase
+public class DocumentCollectionResponseModel : ContentCollectionResponseModelBase
{
public DocumentTypeCollectionReferenceResponseModel DocumentType { get; set; } = new();
diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentResponseModel.cs
index d2eeb2e6af..2854a53320 100644
--- a/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentResponseModel.cs
+++ b/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentResponseModel.cs
@@ -1,6 +1,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.Document;
-public class DocumentResponseModel : DocumentResponseModelBase
+public class DocumentResponseModel : DocumentResponseModelBase
{
public IEnumerable Urls { get; set; } = Enumerable.Empty();
diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentValueResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentValueResponseModel.cs
new file mode 100644
index 0000000000..c112da394c
--- /dev/null
+++ b/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentValueResponseModel.cs
@@ -0,0 +1,7 @@
+using Umbraco.Cms.Core.Models.ContentEditing;
+
+namespace Umbraco.Cms.Api.Management.ViewModels.Document;
+
+public class DocumentValueResponseModel : ValueResponseModelBase
+{
+}
diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentVersionResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentVersionResponseModel.cs
index 486bee306f..5068ab3f40 100644
--- a/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentVersionResponseModel.cs
+++ b/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentVersionResponseModel.cs
@@ -1,6 +1,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.Document;
-public class DocumentVersionResponseModel : DocumentResponseModelBase
+public class DocumentVersionResponseModel : DocumentResponseModelBase
{
public ReferenceByIdModel? Document { get; set; }
}
diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Document/ValidateUpdateDocumentRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Document/ValidateUpdateDocumentRequestModel.cs
new file mode 100644
index 0000000000..806c733cc2
--- /dev/null
+++ b/src/Umbraco.Cms.Api.Management/ViewModels/Document/ValidateUpdateDocumentRequestModel.cs
@@ -0,0 +1,6 @@
+namespace Umbraco.Cms.Api.Management.ViewModels.Document;
+
+public class ValidateUpdateDocumentRequestModel : UpdateDocumentRequestModel
+{
+ public ISet? Cultures { get; set; }
+}
diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/DocumentBlueprint/DocumentBlueprintResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/DocumentBlueprint/DocumentBlueprintResponseModel.cs
index e23cb10f41..7b8a60b6e2 100644
--- a/src/Umbraco.Cms.Api.Management/ViewModels/DocumentBlueprint/DocumentBlueprintResponseModel.cs
+++ b/src/Umbraco.Cms.Api.Management/ViewModels/DocumentBlueprint/DocumentBlueprintResponseModel.cs
@@ -2,6 +2,6 @@ using Umbraco.Cms.Api.Management.ViewModels.Document;
namespace Umbraco.Cms.Api.Management.ViewModels.DocumentBlueprint;
-public class DocumentBlueprintResponseModel : DocumentResponseModelBase
+public class DocumentBlueprintResponseModel : DocumentResponseModelBase
{
}
diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Media/Collection/MediaCollectionResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Media/Collection/MediaCollectionResponseModel.cs
index fb772f858f..72891a9bd1 100644
--- a/src/Umbraco.Cms.Api.Management/ViewModels/Media/Collection/MediaCollectionResponseModel.cs
+++ b/src/Umbraco.Cms.Api.Management/ViewModels/Media/Collection/MediaCollectionResponseModel.cs
@@ -3,7 +3,7 @@ using Umbraco.Cms.Api.Management.ViewModels.MediaType;
namespace Umbraco.Cms.Api.Management.ViewModels.Media.Collection;
-public class MediaCollectionResponseModel : ContentCollectionResponseModelBase
+public class MediaCollectionResponseModel : ContentCollectionResponseModelBase
{
public MediaTypeCollectionReferenceResponseModel MediaType { get; set; } = new();
}
diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Media/MediaResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Media/MediaResponseModel.cs
index dc6bc6da40..8b78b98f2c 100644
--- a/src/Umbraco.Cms.Api.Management/ViewModels/Media/MediaResponseModel.cs
+++ b/src/Umbraco.Cms.Api.Management/ViewModels/Media/MediaResponseModel.cs
@@ -3,7 +3,7 @@ using Umbraco.Cms.Api.Management.ViewModels.MediaType;
namespace Umbraco.Cms.Api.Management.ViewModels.Media;
-public class MediaResponseModel : ContentResponseModelBase
+public class MediaResponseModel : ContentResponseModelBase
{
public IEnumerable Urls { get; set; } = Enumerable.Empty();
diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Media/MediaValueResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Media/MediaValueResponseModel.cs
new file mode 100644
index 0000000000..10ffd04bd0
--- /dev/null
+++ b/src/Umbraco.Cms.Api.Management/ViewModels/Media/MediaValueResponseModel.cs
@@ -0,0 +1,7 @@
+using Umbraco.Cms.Core.Models.ContentEditing;
+
+namespace Umbraco.Cms.Api.Management.ViewModels.Media;
+
+public class MediaValueResponseModel : ValueResponseModelBase
+{
+}
diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Member/Item/MemberItemResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Member/Item/MemberItemResponseModel.cs
index f959a37a90..e55ad04572 100644
--- a/src/Umbraco.Cms.Api.Management/ViewModels/Member/Item/MemberItemResponseModel.cs
+++ b/src/Umbraco.Cms.Api.Management/ViewModels/Member/Item/MemberItemResponseModel.cs
@@ -1,6 +1,7 @@
using Umbraco.Cms.Api.Management.ViewModels.Content;
using Umbraco.Cms.Api.Management.ViewModels.Item;
using Umbraco.Cms.Api.Management.ViewModels.MemberType;
+using Umbraco.Cms.Core.Models.Membership;
namespace Umbraco.Cms.Api.Management.ViewModels.Member.Item;
@@ -9,4 +10,6 @@ public class MemberItemResponseModel : ItemResponseModelBase
public MemberTypeReferenceResponseModel MemberType { get; set; } = new();
public IEnumerable Variants { get; set; } = Enumerable.Empty();
+
+ public MemberKind Kind { get; set; }
}
diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Member/MemberResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Member/MemberResponseModel.cs
index 0125d85523..626d5e6b8e 100644
--- a/src/Umbraco.Cms.Api.Management/ViewModels/Member/MemberResponseModel.cs
+++ b/src/Umbraco.Cms.Api.Management/ViewModels/Member/MemberResponseModel.cs
@@ -1,9 +1,10 @@
using Umbraco.Cms.Api.Management.ViewModels.Content;
using Umbraco.Cms.Api.Management.ViewModels.MemberType;
+using Umbraco.Cms.Core.Models.Membership;
namespace Umbraco.Cms.Api.Management.ViewModels.Member;
-public class MemberResponseModel : ContentResponseModelBase
+public class MemberResponseModel : ContentResponseModelBase
{
public string Email { get; set; } = string.Empty;
@@ -26,4 +27,6 @@ public class MemberResponseModel : ContentResponseModelBase Groups { get; set; } = [];
+
+ public MemberKind Kind { get; set; }
}
diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Member/MemberValueResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Member/MemberValueResponseModel.cs
new file mode 100644
index 0000000000..1db8190ecb
--- /dev/null
+++ b/src/Umbraco.Cms.Api.Management/ViewModels/Member/MemberValueResponseModel.cs
@@ -0,0 +1,7 @@
+using Umbraco.Cms.Core.Models.ContentEditing;
+
+namespace Umbraco.Cms.Api.Management.ViewModels.Member;
+
+public class MemberValueResponseModel : ValueResponseModelBase
+{
+}
diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/User/ClientCredentials/CreateUserClientCredentialsRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/User/ClientCredentials/CreateUserClientCredentialsRequestModel.cs
new file mode 100644
index 0000000000..56340b10ab
--- /dev/null
+++ b/src/Umbraco.Cms.Api.Management/ViewModels/User/ClientCredentials/CreateUserClientCredentialsRequestModel.cs
@@ -0,0 +1,8 @@
+namespace Umbraco.Cms.Api.Management.ViewModels.User.ClientCredentials;
+
+public sealed class CreateUserClientCredentialsRequestModel
+{
+ public required string ClientId { get; set; }
+
+ public required string ClientSecret { get; set; }
+}
diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/User/CreateUserRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/User/CreateUserRequestModel.cs
index fa9b1b856a..3ce44784b9 100644
--- a/src/Umbraco.Cms.Api.Management/ViewModels/User/CreateUserRequestModel.cs
+++ b/src/Umbraco.Cms.Api.Management/ViewModels/User/CreateUserRequestModel.cs
@@ -1,6 +1,8 @@
-namespace Umbraco.Cms.Api.Management.ViewModels.User;
+using Umbraco.Cms.Core.Models.Membership;
-public class CreateUserRequestModel : UserPresentationBase
+namespace Umbraco.Cms.Api.Management.ViewModels.User;
+
+public class CreateUserRequestModel : CreateUserRequestModelBase
{
- public Guid? Id { get; set; }
+ public UserKind Kind { get; set; }
}
diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/User/CreateUserRequestModelBase.cs b/src/Umbraco.Cms.Api.Management/ViewModels/User/CreateUserRequestModelBase.cs
new file mode 100644
index 0000000000..35ccf445ac
--- /dev/null
+++ b/src/Umbraco.Cms.Api.Management/ViewModels/User/CreateUserRequestModelBase.cs
@@ -0,0 +1,6 @@
+namespace Umbraco.Cms.Api.Management.ViewModels.User;
+
+public class CreateUserRequestModelBase : UserPresentationBase
+{
+ public Guid? Id { get; set; }
+}
diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/User/InviteUserRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/User/InviteUserRequestModel.cs
index 9f73d12dea..e41c4d5487 100644
--- a/src/Umbraco.Cms.Api.Management/ViewModels/User/InviteUserRequestModel.cs
+++ b/src/Umbraco.Cms.Api.Management/ViewModels/User/InviteUserRequestModel.cs
@@ -1,6 +1,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.User;
-public class InviteUserRequestModel : CreateUserRequestModel
+public class InviteUserRequestModel : CreateUserRequestModelBase
{
public string? Message { get; set; }
}
diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/User/Item/UserItemResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/User/Item/UserItemResponseModel.cs
index 8fd2618784..4dd8139ba1 100644
--- a/src/Umbraco.Cms.Api.Management/ViewModels/User/Item/UserItemResponseModel.cs
+++ b/src/Umbraco.Cms.Api.Management/ViewModels/User/Item/UserItemResponseModel.cs
@@ -1,8 +1,11 @@
using Umbraco.Cms.Api.Management.ViewModels.Item;
+using Umbraco.Cms.Core.Models.Membership;
namespace Umbraco.Cms.Api.Management.ViewModels.User.Item;
public class UserItemResponseModel : NamedItemResponseModelBase
{
public IEnumerable AvatarUrls { get; set; } = Enumerable.Empty();
+
+ public UserKind Kind { get; set; }
}
diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/User/UserResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/User/UserResponseModel.cs
index bc3fc92c22..8177b02d1e 100644
--- a/src/Umbraco.Cms.Api.Management/ViewModels/User/UserResponseModel.cs
+++ b/src/Umbraco.Cms.Api.Management/ViewModels/User/UserResponseModel.cs
@@ -31,5 +31,8 @@ public class UserResponseModel : UserPresentationBase
public DateTimeOffset? LastLockoutDate { get; set; }
public DateTimeOffset? LastPasswordChangeDate { get; set; }
+
public bool IsAdmin { get; set; }
+
+ public UserKind Kind { get; set; }
}
diff --git a/src/Umbraco.Cms.Imaging.ImageSharp/ConfigureImageSharpMiddlewareOptions.cs b/src/Umbraco.Cms.Imaging.ImageSharp/ConfigureImageSharpMiddlewareOptions.cs
index 9a1ecead89..0b761e125d 100644
--- a/src/Umbraco.Cms.Imaging.ImageSharp/ConfigureImageSharpMiddlewareOptions.cs
+++ b/src/Umbraco.Cms.Imaging.ImageSharp/ConfigureImageSharpMiddlewareOptions.cs
@@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Headers;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;
+using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.Web.Commands;
using SixLabors.ImageSharp.Web.Middleware;
using SixLabors.ImageSharp.Web.Processors;
@@ -85,5 +86,14 @@ public sealed class ConfigureImageSharpMiddlewareOptions : IConfigureOptions
+
+
+
+
+
+
+
diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Umbraco.Cms.Persistence.SqlServer.csproj b/src/Umbraco.Cms.Persistence.SqlServer/Umbraco.Cms.Persistence.SqlServer.csproj
index b6a342b7b8..d5edbbee4c 100644
--- a/src/Umbraco.Cms.Persistence.SqlServer/Umbraco.Cms.Persistence.SqlServer.csproj
+++ b/src/Umbraco.Cms.Persistence.SqlServer/Umbraco.Cms.Persistence.SqlServer.csproj
@@ -21,6 +21,12 @@
+
+
+
+
+
+
diff --git a/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoLogin/Index.cshtml b/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoLogin/Index.cshtml
index 856796cd25..716521066c 100644
--- a/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoLogin/Index.cshtml
+++ b/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoLogin/Index.cshtml
@@ -31,7 +31,6 @@
var allowPasswordReset = SecuritySettings.Value.AllowPasswordReset && EmailSender.CanSendRequiredEmail();
var disableLocalLogin = ExternalLogins.HasDenyLocalLogin();
}
-@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@@ -45,7 +44,7 @@
Umbraco
-
+
@await Html.BackOfficeImportMapScriptAsync(JsonSerializer, BackOfficePathGenerator, PackageManifestService)
-
+
diff --git a/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoWebsite/Maintenance.cshtml b/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoWebsite/Maintenance.cshtml
index 94de5f3c52..5c41abf6fa 100644
--- a/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoWebsite/Maintenance.cshtml
+++ b/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoWebsite/Maintenance.cshtml
@@ -17,7 +17,7 @@
Website is Under Maintainance
-
+