diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Tour/GetTourController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Tour/GetTourController.cs deleted file mode 100644 index 9d223a3f72..0000000000 --- a/src/Umbraco.Cms.Api.Management/Controllers/Tour/GetTourController.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Asp.Versioning; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Umbraco.Cms.Api.Management.ViewModels.Tour; -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; - -namespace Umbraco.Cms.Api.Management.Controllers.Tour; - -[ApiVersion("1.0")] -public class GetTourController : TourControllerBase -{ - private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; - private readonly ITourService _tourService; - private readonly IUmbracoMapper _umbracoMapper; - - public GetTourController( - IBackOfficeSecurityAccessor backOfficeSecurityAccessor, - ITourService tourService, - IUmbracoMapper umbracoMapper) - { - _backOfficeSecurityAccessor = backOfficeSecurityAccessor; - _tourService = tourService; - _umbracoMapper = umbracoMapper; - } - - [HttpGet] - [MapToApiVersion("1.0")] - [ProducesResponseType(typeof(UserTourStatusesResponseModel), StatusCodes.Status200OK)] - public async Task GetTours(CancellationToken cancellationToken) - { - Guid currentUserKey = CurrentUserKey(_backOfficeSecurityAccessor); - Attempt, TourOperationStatus> toursAttempt = await _tourService.GetAllAsync(currentUserKey); - - if (toursAttempt.Success == false) - { - return TourOperationStatusResult(toursAttempt.Status); - } - - List models = _umbracoMapper.MapEnumerable(toursAttempt.Result); - return Ok(new UserTourStatusesResponseModel { TourStatuses = models }); - } -} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Tour/SetTourController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Tour/SetTourController.cs deleted file mode 100644 index 91ae09c011..0000000000 --- a/src/Umbraco.Cms.Api.Management/Controllers/Tour/SetTourController.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Asp.Versioning; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Umbraco.Cms.Api.Management.ViewModels.Tour; -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; - -namespace Umbraco.Cms.Api.Management.Controllers.Tour; - -[ApiVersion("1.0")] -public class SetTourController : TourControllerBase -{ - private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; - private readonly ITourService _tourService; - private readonly IUmbracoMapper _umbracoMapper; - - public SetTourController( - IBackOfficeSecurityAccessor backOfficeSecurityAccessor, - ITourService tourService, - IUmbracoMapper umbracoMapper) - { - _backOfficeSecurityAccessor = backOfficeSecurityAccessor; - _tourService = tourService; - _umbracoMapper = umbracoMapper; - } - - [HttpPost] - [MapToApiVersion("1.0")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task SetTour(CancellationToken cancellationToken, SetTourStatusRequestModel model) - { - Guid currentUserKey = CurrentUserKey(_backOfficeSecurityAccessor); - - UserTourStatus tourStatus = _umbracoMapper.Map(model)!; - - TourOperationStatus attempt = await _tourService.SetAsync(tourStatus, currentUserKey); - return TourOperationStatusResult(attempt); - } -} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Tour/TourControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Tour/TourControllerBase.cs deleted file mode 100644 index fafb01ed17..0000000000 --- a/src/Umbraco.Cms.Api.Management/Controllers/Tour/TourControllerBase.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Umbraco.Cms.Api.Management.Routing; -using Umbraco.Cms.Core.Services.OperationStatus; - -namespace Umbraco.Cms.Api.Management.Controllers.Tour; - -[VersionedApiBackOfficeRoute("tour")] -[ApiExplorerSettings(GroupName = "Tour")] -public class TourControllerBase : ManagementApiControllerBase -{ - protected IActionResult TourOperationStatusResult(TourOperationStatus status) => - OperationStatusResult(status, problemDetailsBuilder => status switch - { - TourOperationStatus.Success => Ok(), - TourOperationStatus.UserNotFound => NotFound(problemDetailsBuilder - .WithTitle("User not found") - .WithDetail("Was not able to find currently logged in user") - .Build()), - _ => StatusCode(StatusCodes.Status500InternalServerError, problemDetailsBuilder - .WithTitle("Unknown tour operation status.") - .Build()), - }); -} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/UserData/ByKeyUserDataController.cs b/src/Umbraco.Cms.Api.Management/Controllers/UserData/ByKeyUserDataController.cs new file mode 100644 index 0000000000..ef593a565a --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/UserData/ByKeyUserDataController.cs @@ -0,0 +1,51 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.ViewModels.UserData; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Persistence.Querying; + +namespace Umbraco.Cms.Api.Management.Controllers.UserData; + +[ApiVersion("1.0")] +public class ByKeyUserDataController : UserDataControllerBase +{ + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + private readonly IUserDataService _userDataService; + private readonly IUmbracoMapper _umbracoMapper; + + public ByKeyUserDataController( + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IUserDataService userDataService, + IUmbracoMapper umbracoMapper) + { + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + _userDataService = userDataService; + _umbracoMapper = umbracoMapper; + } + + [HttpGet("{id:guid}")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(UserDataViewModel), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task ByKey(CancellationToken cancellationToken, Guid id) + { + Guid currentUserKey = CurrentUserKey(_backOfficeSecurityAccessor); + IUserData? data = await _userDataService.GetAsync(id); + if (data is null) + { + return NotFound(); + } + + if (data.UserKey != currentUserKey) + { + return Unauthorized(); + } + + return Ok(_umbracoMapper.Map(data)); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/UserData/CreateUserDataController.cs b/src/Umbraco.Cms.Api.Management/Controllers/UserData/CreateUserDataController.cs new file mode 100644 index 0000000000..da5318038d --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/UserData/CreateUserDataController.cs @@ -0,0 +1,50 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.ViewModels.UserData; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.UserData; + +[ApiVersion("1.0")] +public class CreateUserDataController : UserDataControllerBase +{ + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + private readonly IUserDataService _userDataService; + private readonly IUmbracoMapper _umbracoMapper; + + public CreateUserDataController( + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IUserDataService userDataService, + IUmbracoMapper umbracoMapper) + { + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + _userDataService = userDataService; + _umbracoMapper = umbracoMapper; + } + + [HttpPost] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(typeof(UserDataOperationStatus), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(UserDataOperationStatus), StatusCodes.Status404NotFound)] + public async Task Create(CancellationToken cancellationToken, CreateUserDataRequestModel model) + { + Guid currentUserKey = CurrentUserKey(_backOfficeSecurityAccessor); + + IUserData userData = _umbracoMapper.Map(model)!; + userData.UserKey = currentUserKey; + + Attempt attempt = await _userDataService.CreateAsync(userData); + + + return attempt.Success + ? CreatedAtId(controller => nameof(controller.ByKey), attempt.Result.Key) + : UserDataOperationStatusResult(attempt.Status); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/UserData/GetUserDataController.cs b/src/Umbraco.Cms.Api.Management/Controllers/UserData/GetUserDataController.cs new file mode 100644 index 0000000000..9f8affc883 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/UserData/GetUserDataController.cs @@ -0,0 +1,50 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.ViewModels.UserData; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Persistence.Querying; + +namespace Umbraco.Cms.Api.Management.Controllers.UserData; + +[ApiVersion("1.0")] +public class GetUserDataController : UserDataControllerBase +{ + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + private readonly IUserDataService _userDataService; + private readonly IUmbracoMapper _umbracoMapper; + + public GetUserDataController( + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IUserDataService userDataService, + IUmbracoMapper umbracoMapper) + { + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + _userDataService = userDataService; + _umbracoMapper = umbracoMapper; + } + + [HttpGet] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task GetUserData(CancellationToken cancellationToken, [FromQuery]string[]? groups, [FromQuery]string[]? identifiers, [FromQuery]int skip = 0, [FromQuery]int take = 100) + { + Guid currentUserKey = CurrentUserKey(_backOfficeSecurityAccessor); + + PagedModel data = await _userDataService.GetAsync( + skip, + take, + new UserDataFilter { UserKeys = new[] { currentUserKey }, Groups = groups, Identifiers = identifiers }); + + return Ok(new PagedViewModel + { + Total = data.Total, + Items = _umbracoMapper.MapEnumerable(data.Items), + }); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/UserData/UpdateUserDataController.cs b/src/Umbraco.Cms.Api.Management/Controllers/UserData/UpdateUserDataController.cs new file mode 100644 index 0000000000..2969ecdf48 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/UserData/UpdateUserDataController.cs @@ -0,0 +1,49 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.ViewModels.UserData; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.UserData; + +[ApiVersion("1.0")] +public class UpdateUserDataController : UserDataControllerBase +{ + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + private readonly IUserDataService _userDataService; + private readonly IUmbracoMapper _umbracoMapper; + + public UpdateUserDataController( + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IUserDataService userDataService, + IUmbracoMapper umbracoMapper) + { + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + _userDataService = userDataService; + _umbracoMapper = umbracoMapper; + } + + [HttpPut] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(UserDataOperationStatus), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(UserDataOperationStatus), StatusCodes.Status404NotFound)] + public async Task Create(CancellationToken cancellationToken, UpdateUserDataRequestModel model) + { + Guid currentUserKey = CurrentUserKey(_backOfficeSecurityAccessor); + + IUserData userData = _umbracoMapper.Map(model)!; + userData.UserKey = currentUserKey; + + Attempt attempt = await _userDataService.UpdateAsync(userData); + + return attempt.Success + ? Ok(attempt.Result) + : UserDataOperationStatusResult(attempt.Status); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/UserData/UserDataControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/UserData/UserDataControllerBase.cs new file mode 100644 index 0000000000..3e9b789b09 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/UserData/UserDataControllerBase.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Routing; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.UserData; + +[VersionedApiBackOfficeRoute("user-data")] +[ApiExplorerSettings(GroupName = "User Data")] +public class UserDataControllerBase : ManagementApiControllerBase +{ + protected IActionResult UserDataOperationStatusResult(UserDataOperationStatus status) => + OperationStatusResult(status, problemDetailsBuilder => status switch + { + UserDataOperationStatus.Success => Ok(), + UserDataOperationStatus.UserNotFound => NotFound(problemDetailsBuilder + .WithTitle("User not found") + .Build()), + UserDataOperationStatus.NotFound => NotFound(problemDetailsBuilder + .WithTitle("UserData not found") + .Build()), + UserDataOperationStatus.AlreadyExists => BadRequest(problemDetailsBuilder + .WithTitle("UserData already exists") + .WithDetail("A userData entry with the given key already exists") + .Build()), + _ => StatusCode(StatusCodes.Status500InternalServerError, problemDetailsBuilder + .WithTitle("Unknown userData operation status.") + .Build()), + }); +} diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/TourBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/TourBuilderExtensions.cs deleted file mode 100644 index 949a650608..0000000000 --- a/src/Umbraco.Cms.Api.Management/DependencyInjection/TourBuilderExtensions.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Umbraco.Cms.Api.Management.Mapping.Tour; -using Umbraco.Cms.Core.DependencyInjection; -using Umbraco.Cms.Core.Mapping; - -namespace Umbraco.Cms.Api.Management.DependencyInjection; - -internal static class TourBuilderExtensions -{ - internal static IUmbracoBuilder AddTours(this IUmbracoBuilder builder) - { - builder.WithCollectionBuilder() - .Add(); - - return builder; - } -} diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilderExtensions.cs index c438fc8048..8aa87f61fb 100644 --- a/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilderExtensions.cs @@ -54,7 +54,6 @@ public static partial class UmbracoBuilderExtensions .AddLogViewer() .AddUsers() .AddUserGroups() - .AddTours() .AddPackages() .AddEntities() .AddScripts() @@ -66,7 +65,8 @@ public static partial class UmbracoBuilderExtensions .AddWebhooks() .AddPreview() .AddPasswordConfiguration() - .AddSupplemenataryLocalizedTextFileSources(); + .AddSupplemenataryLocalizedTextFileSources() + .AddUserData(); services .ConfigureOptions() diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/UserDataBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/UserDataBuilderExtensions.cs new file mode 100644 index 0000000000..31bd28dabf --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/UserDataBuilderExtensions.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.Mapping.Dictionary; +using Umbraco.Cms.Api.Management.Mapping.UserData; +using Umbraco.Cms.Api.Management.Services; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Mapping; + +namespace Umbraco.Cms.Api.Management.DependencyInjection; + +internal static class UserDataBuilderExtensions +{ + internal static IUmbracoBuilder AddUserData(this IUmbracoBuilder builder) + { + builder.WithCollectionBuilder().Add(); + + return builder; + } +} diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Tour/TourViewModelsMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/Tour/TourViewModelsMapDefinition.cs deleted file mode 100644 index be260155a9..0000000000 --- a/src/Umbraco.Cms.Api.Management/Mapping/Tour/TourViewModelsMapDefinition.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Umbraco.Cms.Api.Management.ViewModels.Tour; -using Umbraco.Cms.Core.Mapping; -using Umbraco.Cms.Core.Models; - -namespace Umbraco.Cms.Api.Management.Mapping.Tour; - -public class TourViewModelsMapDefinition : IMapDefinition -{ - public void DefineMaps(IUmbracoMapper mapper) - { - mapper.Define((_, _) => new TourStatusViewModel{ Alias = string.Empty}, Map); - mapper.Define((_, _) => new UserTourStatus(), Map); - } - - // Umbraco.Code.MapAll - private void Map(SetTourStatusRequestModel source, UserTourStatus target, MapperContext context) - { - target.Alias = source.Alias; - target.Completed = source.Completed; - target.Disabled = source.Disabled; - } - - // Umbraco.Code.MapAll - private void Map(UserTourStatus source, TourStatusViewModel target, MapperContext context) - { - target.Alias = source.Alias; - target.Completed = source.Completed; - target.Disabled = source.Disabled; - } -} diff --git a/src/Umbraco.Cms.Api.Management/Mapping/UserData/UserDataMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/UserData/UserDataMapDefinition.cs new file mode 100644 index 0000000000..0616bf4be6 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Mapping/UserData/UserDataMapDefinition.cs @@ -0,0 +1,42 @@ +using Umbraco.Cms.Api.Management.ViewModels.UserData; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models.Membership; + +namespace Umbraco.Cms.Api.Management.Mapping.UserData; + +public class UserDataMapDefinition : IMapDefinition +{ + public void DefineMaps(IUmbracoMapper mapper) + { + mapper.Define((_, _) => new UserDataResponseModel(), Map); + mapper.Define((_, _) => new Core.Models.Membership.UserData(), Map); + mapper.Define((_, _) => new Core.Models.Membership.UserData(), Map); + } + + private void Map(IUserData source, UserDataResponseModel target, MapperContext context) + { + target.Key = source.Key; + target.Group = source.Group; + target.Identifier = source.Identifier; + target.Value = source.Value; + } + + private void Map(CreateUserDataRequestModel source, IUserData target, MapperContext context) + { + target.Key = source.Key ?? Guid.NewGuid(); + MapBase(source, target, context); + } + + private void Map(UpdateUserDataRequestModel source, IUserData target, MapperContext context) + { + target.Key = source.Key; + MapBase(source, target, context); + } + + private void MapBase(UserDataViewModel source, IUserData target, MapperContext context) + { + target.Group = source.Group; + target.Identifier = source.Identifier; + target.Value = source.Value; + } +} diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index a0a4f54327..67b88e509f 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -33513,118 +33513,6 @@ ] } }, - "/umbraco/management/api/v1/tour": { - "get": { - "tags": [ - "Tour" - ], - "operationId": "GetTour", - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "$ref": "#/components/schemas/UserTourStatusesResponseModel" - } - ] - } - }, - "text/json": { - "schema": { - "oneOf": [ - { - "$ref": "#/components/schemas/UserTourStatusesResponseModel" - } - ] - } - }, - "text/plain": { - "schema": { - "oneOf": [ - { - "$ref": "#/components/schemas/UserTourStatusesResponseModel" - } - ] - } - } - } - }, - "401": { - "description": "The resource is protected and requires an authentication token" - } - }, - "security": [ - { - "Backoffice User": [ ] - } - ] - }, - "post": { - "tags": [ - "Tour" - ], - "operationId": "PostTour", - "requestBody": { - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "$ref": "#/components/schemas/SetTourStatusRequestModel" - } - ] - } - }, - "text/json": { - "schema": { - "oneOf": [ - { - "$ref": "#/components/schemas/SetTourStatusRequestModel" - } - ] - } - }, - "application/*+json": { - "schema": { - "oneOf": [ - { - "$ref": "#/components/schemas/SetTourStatusRequestModel" - } - ] - } - } - } - }, - "responses": { - "200": { - "description": "Success", - "headers": { - "Umb-Notifications": { - "description": "The list of notifications produced during the request.", - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/NotificationHeaderModel" - }, - "nullable": true - } - } - } - }, - "401": { - "description": "The resource is protected and requires an authentication token" - } - }, - "security": [ - { - "Backoffice User": [ ] - } - ] - } - }, "/umbraco/management/api/v1/upgrade/authorize": { "post": { "tags": [ @@ -33828,6 +33716,427 @@ ] } }, + "/umbraco/management/api/v1/user-data": { + "post": { + "tags": [ + "User Data" + ], + "operationId": "PostUserData", + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/CreateUserDataRequestModel" + } + ] + } + }, + "text/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/CreateUserDataRequestModel" + } + ] + } + }, + "application/*+json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/CreateUserDataRequestModel" + } + ] + } + } + } + }, + "responses": { + "201": { + "description": "Created", + "headers": { + "Umb-Generated-Resource": { + "description": "Identifier of the newly created resource", + "schema": { + "type": "string", + "description": "Identifier of the newly created resource" + } + }, + "Location": { + "description": "Location of the newly created resource", + "schema": { + "type": "string", + "description": "Location of the newly created resource", + "format": "uri" + } + }, + "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": { + "$ref": "#/components/schemas/UserDataOperationStatusModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/UserDataOperationStatusModel" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/UserDataOperationStatusModel" + } + } + } + }, + "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": { + "$ref": "#/components/schemas/UserDataOperationStatusModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/UserDataOperationStatusModel" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/UserDataOperationStatusModel" + } + } + } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + } + }, + "security": [ + { + "Backoffice User": [ ] + } + ] + }, + "put": { + "tags": [ + "User Data" + ], + "operationId": "PutUserData", + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/UpdateUserDataRequestModel" + } + ] + } + }, + "text/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/UpdateUserDataRequestModel" + } + ] + } + }, + "application/*+json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/UpdateUserDataRequestModel" + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "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": { + "$ref": "#/components/schemas/UserDataOperationStatusModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/UserDataOperationStatusModel" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/UserDataOperationStatusModel" + } + } + } + }, + "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": { + "$ref": "#/components/schemas/UserDataOperationStatusModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/UserDataOperationStatusModel" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/UserDataOperationStatusModel" + } + } + } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + } + }, + "security": [ + { + "Backoffice User": [ ] + } + ] + }, + "get": { + "tags": [ + "User Data" + ], + "operationId": "GetUserData", + "parameters": [ + { + "name": "groups", + "in": "query", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "identifiers", + "in": "query", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + } + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/PagedUserDataResponseModel" + } + ] + } + }, + "text/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/PagedUserDataResponseModel" + } + ] + } + }, + "text/plain": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/PagedUserDataResponseModel" + } + ] + } + } + } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + } + }, + "security": [ + { + "Backoffice User": [ ] + } + ] + } + }, + "/umbraco/management/api/v1/user-data/{id}": { + "get": { + "tags": [ + "User Data" + ], + "operationId": "GetUserDataById", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/UserDataModel" + } + ] + } + }, + "text/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/UserDataModel" + } + ] + } + }, + "text/plain": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/UserDataModel" + } + ] + } + } + } + }, + "404": { + "description": "Not Found" + }, + "401": { + "description": "The resource is protected and requires an authentication token" + } + }, + "security": [ + { + "Backoffice User": [ ] + } + ] + } + }, "/umbraco/management/api/v1/item/user-group": { "get": { "tags": [ @@ -41654,6 +41963,31 @@ }, "additionalProperties": false }, + "CreateUserDataRequestModel": { + "required": [ + "group", + "identifier", + "value" + ], + "type": "object", + "properties": { + "group": { + "type": "string" + }, + "identifier": { + "type": "string" + }, + "value": { + "type": "string" + }, + "key": { + "type": "string", + "format": "uuid", + "nullable": true + } + }, + "additionalProperties": false + }, "CreateUserGroupRequestModel": { "required": [ "documentRootAccess", @@ -47602,6 +47936,30 @@ }, "additionalProperties": false }, + "PagedUserDataResponseModel": { + "required": [ + "items", + "total" + ], + "type": "object", + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "items": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/UserDataResponseModel" + } + ] + } + } + }, + "additionalProperties": false + }, "PagedUserGroupResponseModel": { "required": [ "items", @@ -48604,26 +48962,6 @@ }, "additionalProperties": false }, - "SetTourStatusRequestModel": { - "required": [ - "alias", - "completed", - "disabled" - ], - "type": "object", - "properties": { - "alias": { - "type": "string" - }, - "completed": { - "type": "boolean" - }, - "disabled": { - "type": "boolean" - } - }, - "additionalProperties": false - }, "SortingRequestModel": { "required": [ "sorting" @@ -49162,26 +49500,6 @@ }, "additionalProperties": false }, - "TourStatusModel": { - "required": [ - "alias", - "completed", - "disabled" - ], - "type": "object", - "properties": { - "alias": { - "type": "string" - }, - "completed": { - "type": "boolean" - }, - "disabled": { - "type": "boolean" - } - }, - "additionalProperties": false - }, "TrackedReferenceDocumentTypeModel": { "type": "object", "properties": { @@ -50378,6 +50696,31 @@ }, "additionalProperties": false }, + "UpdateUserDataRequestModel": { + "required": [ + "group", + "identifier", + "key", + "value" + ], + "type": "object", + "properties": { + "group": { + "type": "string" + }, + "identifier": { + "type": "string" + }, + "value": { + "type": "string" + }, + "key": { + "type": "string", + "format": "uuid" + } + }, + "additionalProperties": false + }, "UpdateUserGroupRequestModel": { "required": [ "documentRootAccess", @@ -50628,6 +50971,60 @@ }, "additionalProperties": false }, + "UserDataModel": { + "required": [ + "group", + "identifier", + "value" + ], + "type": "object", + "properties": { + "group": { + "type": "string" + }, + "identifier": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + }, + "UserDataOperationStatusModel": { + "enum": [ + "Success", + "NotFound", + "UserNotFound", + "AlreadyExists" + ], + "type": "string" + }, + "UserDataResponseModel": { + "required": [ + "group", + "identifier", + "key", + "value" + ], + "type": "object", + "properties": { + "group": { + "type": "string" + }, + "identifier": { + "type": "string" + }, + "value": { + "type": "string" + }, + "key": { + "type": "string", + "format": "uuid" + } + }, + "additionalProperties": false + }, "UserGroupItemResponseModel": { "required": [ "id", @@ -50982,25 +51379,6 @@ ], "type": "string" }, - "UserTourStatusesResponseModel": { - "required": [ - "tourStatuses" - ], - "type": "object", - "properties": { - "tourStatuses": { - "type": "array", - "items": { - "oneOf": [ - { - "$ref": "#/components/schemas/TourStatusModel" - } - ] - } - } - }, - "additionalProperties": false - }, "UserTwoFactorProviderModel": { "required": [ "isEnabledOnUser", diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Tour/SetTourStatusRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Tour/SetTourStatusRequestModel.cs deleted file mode 100644 index ca6e454826..0000000000 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Tour/SetTourStatusRequestModel.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Umbraco.Cms.Api.Management.ViewModels.Tour; - -public class SetTourStatusRequestModel : TourStatusViewModel -{ - -} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Tour/TourStatusResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Tour/TourStatusResponseModel.cs deleted file mode 100644 index 909f484244..0000000000 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Tour/TourStatusResponseModel.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Umbraco.Cms.Api.Management.ViewModels.Tour; - -public class UserTourStatusesResponseModel -{ - public required IEnumerable TourStatuses { get; set; } -} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Tour/TourStatusViewModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Tour/TourStatusViewModel.cs deleted file mode 100644 index 3e20b7eb36..0000000000 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Tour/TourStatusViewModel.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Umbraco.Cms.Api.Management.ViewModels.Tour; - -public class TourStatusViewModel -{ - public required string Alias { get; set; } - - public bool Completed { get; set; } - - public bool Disabled { get; set; } -} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/UserData/CreateUserDataRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/UserData/CreateUserDataRequestModel.cs new file mode 100644 index 0000000000..829a6a47c5 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/UserData/CreateUserDataRequestModel.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.UserData; + +public class CreateUserDataRequestModel : UserDataViewModel +{ + public Guid? Key { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/UserData/UpdateUserDataRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/UserData/UpdateUserDataRequestModel.cs new file mode 100644 index 0000000000..90c9da1e2e --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/UserData/UpdateUserDataRequestModel.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.UserData; + +public class UpdateUserDataRequestModel : UserDataViewModel +{ + public Guid Key { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/UserData/UserDataResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/UserData/UserDataResponseModel.cs new file mode 100644 index 0000000000..4189fd9771 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/UserData/UserDataResponseModel.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.UserData; + +public class UserDataResponseModel : UserDataViewModel +{ + public Guid Key { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/UserData/UserDataViewModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/UserData/UserDataViewModel.cs new file mode 100644 index 0000000000..33be862cfb --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/UserData/UserDataViewModel.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.UserData; + +public class UserDataViewModel +{ + public string Group { get; set; } = string.Empty; + + public string Identifier { get; set; } = string.Empty; + + public string Value { get; set; } = string.Empty; +} diff --git a/src/Umbraco.Core/Configuration/Models/TourSettings.cs b/src/Umbraco.Core/Configuration/Models/TourSettings.cs deleted file mode 100644 index aaf2063c64..0000000000 --- a/src/Umbraco.Core/Configuration/Models/TourSettings.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) Umbraco. -// See LICENSE for more details. - -using System.ComponentModel; - -namespace Umbraco.Cms.Core.Configuration.Models; - -/// -/// Typed configuration options for tour settings. -/// -[UmbracoOptions(Constants.Configuration.ConfigTours)] -public class TourSettings -{ - internal const bool StaticEnableTours = true; - - /// - /// Gets or sets a value indicating whether back-office tours are enabled. - /// - [DefaultValue(StaticEnableTours)] - public bool EnableTours { get; set; } = StaticEnableTours; -} diff --git a/src/Umbraco.Core/Constants-Configuration.cs b/src/Umbraco.Core/Constants-Configuration.cs index 4dfb89fff9..e864b6041c 100644 --- a/src/Umbraco.Core/Constants-Configuration.cs +++ b/src/Umbraco.Core/Constants-Configuration.cs @@ -52,7 +52,6 @@ public static partial class Constants public const string ConfigRuntimeMinificationVersion = ConfigRuntimeMinification + ":Version"; public const string ConfigSecurity = ConfigPrefix + "Security"; public const string ConfigBasicAuth = ConfigPrefix + "BasicAuth"; - public const string ConfigTours = ConfigPrefix + "Tours"; public const string ConfigTypeFinder = ConfigPrefix + "TypeFinder"; public const string ConfigWebRouting = ConfigPrefix + "WebRouting"; public const string ConfigUserPassword = ConfigPrefix + "Security:UserPassword"; diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs index bb4a4d98c3..1fe9383ee3 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs @@ -14,7 +14,6 @@ using Umbraco.Cms.Core.PropertyEditors.Validators; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Snippets; using Umbraco.Cms.Core.Strings; -using Umbraco.Cms.Core.Tour; using Umbraco.Cms.Core.Webhooks; using Umbraco.Extensions; @@ -46,7 +45,6 @@ public static partial class UmbracoBuilderExtensions builder.EditorValidators().Add(() => builder.TypeLoader.GetTypes()); builder.HealthChecks().Add(() => builder.TypeLoader.GetTypes()); builder.HealthCheckNotificationMethods().Add(() => builder.TypeLoader.GetTypes()); - builder.TourFilters(); builder.UrlProviders() .Append() .Append(); @@ -133,12 +131,6 @@ public static partial class UmbracoBuilderExtensions public static HealthCheckNotificationMethodCollectionBuilder HealthCheckNotificationMethods(this IUmbracoBuilder builder) => builder.WithCollectionBuilder(); - /// - /// Gets the TourFilters collection builder. - /// - public static TourFilterCollectionBuilder TourFilters(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); - /// /// Gets the URL providers collection builder. /// diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs index 1850e531a8..8e1817a40a 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs @@ -73,7 +73,6 @@ public static partial class UmbracoBuilderExtensions .AddUmbracoOptions() .AddUmbracoOptions() .AddUmbracoOptions() - .AddUmbracoOptions() .AddUmbracoOptions() .AddUmbracoOptions() .AddUmbracoOptions() diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index 010714cc54..15330ff1e8 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -276,7 +276,6 @@ namespace Umbraco.Cms.Core.DependencyInjection Services.AddUnique(); Services.AddUnique(); Services.AddUnique(); - Services.AddUnique(); Services.AddUnique(); Services.AddUnique(); Services.AddUnique(); diff --git a/src/Umbraco.Core/Models/BackOfficeTour.cs b/src/Umbraco.Core/Models/BackOfficeTour.cs deleted file mode 100644 index a7a9d3a5c3..0000000000 --- a/src/Umbraco.Core/Models/BackOfficeTour.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Runtime.Serialization; - -namespace Umbraco.Cms.Core.Models; - -/// -/// A model representing a tour. -/// -[DataContract(Name = "tour", Namespace = "")] -public class BackOfficeTour -{ - public BackOfficeTour() => RequiredSections = new List(); - - [DataMember(Name = "name")] - public string? Name { get; set; } - - [DataMember(Name = "alias")] - public string Alias { get; set; } = null!; - - [DataMember(Name = "group")] - public string? Group { get; set; } - - [DataMember(Name = "groupOrder")] - public int GroupOrder { get; set; } - - [DataMember(Name = "hidden")] - public bool Hidden { get; set; } - - [DataMember(Name = "allowDisable")] - public bool AllowDisable { get; set; } - - [DataMember(Name = "requiredSections")] - public List RequiredSections { get; set; } - - [DataMember(Name = "steps")] - public BackOfficeTourStep[]? Steps { get; set; } - - [DataMember(Name = "culture")] - public string? Culture { get; set; } - - [DataMember(Name = "contentType")] - public string? ContentType { get; set; } -} diff --git a/src/Umbraco.Core/Models/BackOfficeTourFile.cs b/src/Umbraco.Core/Models/BackOfficeTourFile.cs deleted file mode 100644 index bc0a5cea3b..0000000000 --- a/src/Umbraco.Core/Models/BackOfficeTourFile.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Runtime.Serialization; - -namespace Umbraco.Cms.Core.Models; - -/// -/// A model representing the file used to load a tour. -/// -[DataContract(Name = "tourFile", Namespace = "")] -public class BackOfficeTourFile -{ - public BackOfficeTourFile() => Tours = new List(); - - /// - /// The file name for the tour - /// - [DataMember(Name = "fileName")] - public string? FileName { get; set; } - - /// - /// The plugin folder that the tour comes from - /// - /// - /// If this is null it means it's a Core tour - /// - [DataMember(Name = "pluginName")] - public string? PluginName { get; set; } - - [DataMember(Name = "tours")] - public IEnumerable Tours { get; set; } -} diff --git a/src/Umbraco.Core/Models/BackOfficeTourStep.cs b/src/Umbraco.Core/Models/BackOfficeTourStep.cs deleted file mode 100644 index 296dcf8bc4..0000000000 --- a/src/Umbraco.Core/Models/BackOfficeTourStep.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Runtime.Serialization; - -namespace Umbraco.Cms.Core.Models; - -/// -/// A model representing a step in a tour. -/// -[DataContract(Name = "step", Namespace = "")] -public class BackOfficeTourStep -{ - [DataMember(Name = "title")] - public string? Title { get; set; } - - [DataMember(Name = "content")] - public string? Content { get; set; } - - [DataMember(Name = "type")] - public string? Type { get; set; } - - [DataMember(Name = "element")] - public string? Element { get; set; } - - [DataMember(Name = "elementPreventClick")] - public bool ElementPreventClick { get; set; } - - [DataMember(Name = "backdropOpacity")] - public float? BackdropOpacity { get; set; } - - [DataMember(Name = "event")] - public string? Event { get; set; } - - [DataMember(Name = "view")] - public string? View { get; set; } - - [DataMember(Name = "eventElement")] - public string? EventElement { get; set; } - - [DataMember(Name = "customProperties")] - public object? CustomProperties { get; set; } - - [DataMember(Name = "skipStepIfVisible")] - public string? SkipStepIfVisible { get; set; } -} diff --git a/src/Umbraco.Core/Models/Membership/IUser.cs b/src/Umbraco.Core/Models/Membership/IUser.cs index 6fc409a0c0..f40c1c5c8a 100644 --- a/src/Umbraco.Core/Models/Membership/IUser.cs +++ b/src/Umbraco.Core/Models/Membership/IUser.cs @@ -39,11 +39,6 @@ public interface IUser : IMembershipUser, IRememberBeingDirty /// string? Avatar { get; set; } - /// - /// A Json blob stored for recording tour data for a user - /// - string? TourData { get; set; } - void RemoveGroup(string group); void ClearGroups(); diff --git a/src/Umbraco.Core/Models/Membership/IUserData.cs b/src/Umbraco.Core/Models/Membership/IUserData.cs new file mode 100644 index 0000000000..f12fb962be --- /dev/null +++ b/src/Umbraco.Core/Models/Membership/IUserData.cs @@ -0,0 +1,14 @@ +namespace Umbraco.Cms.Core.Models.Membership; + +public interface IUserData +{ + public Guid Key { get; set; } + + public Guid UserKey { get; set; } + + public string Group { get; set; } + + public string Identifier { get; set; } + + public string Value { get; set; } +} diff --git a/src/Umbraco.Core/Models/Membership/User.cs b/src/Umbraco.Core/Models/Membership/User.cs index 4607b7c811..acc871d43b 100644 --- a/src/Umbraco.Core/Models/Membership/User.cs +++ b/src/Umbraco.Core/Models/Membership/User.cs @@ -38,7 +38,6 @@ public class User : EntityBase, IUser, IProfile private int _sessionTimeout; private int[]? _startContentIds; private int[]? _startMediaIds; - private string? _tourData; private HashSet _userGroups; private string _username; @@ -310,16 +309,6 @@ public class User : EntityBase, IUser, IProfile set => SetPropertyValueAndDetectChanges(value, ref _avatar, nameof(Avatar)); } - /// - /// A Json blob stored for recording tour data for a user - /// - [DataMember] - public string? TourData - { - get => _tourData; - set => SetPropertyValueAndDetectChanges(value, ref _tourData, nameof(TourData)); - } - /// /// Gets or sets the session timeout. /// diff --git a/src/Umbraco.Core/Models/Membership/UserData.cs b/src/Umbraco.Core/Models/Membership/UserData.cs new file mode 100644 index 0000000000..035994b90d --- /dev/null +++ b/src/Umbraco.Core/Models/Membership/UserData.cs @@ -0,0 +1,14 @@ +namespace Umbraco.Cms.Core.Models.Membership; + +public class UserData : IUserData +{ + public Guid Key { get; set; } + + public Guid UserKey { get; set; } + + public string Group { get; set; } = string.Empty; + + public string Identifier { get; set; } = string.Empty; + + public string Value { get; set; } = string.Empty; +} diff --git a/src/Umbraco.Core/Models/UserData.cs b/src/Umbraco.Core/Models/UserData.cs deleted file mode 100644 index 144871c3f7..0000000000 --- a/src/Umbraco.Core/Models/UserData.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Runtime.Serialization; - -namespace Umbraco.Cms.Core.Models; - -[DataContract] -public class UserData -{ - public UserData(string name, string data) - { - Name = name; - Data = data; - } - - [DataMember(Name = "name")] - public string Name { get; } - - [DataMember(Name = "data")] - public string Data { get; } -} diff --git a/src/Umbraco.Core/Models/UserTourStatus.cs b/src/Umbraco.Core/Models/UserTourStatus.cs deleted file mode 100644 index a954a0b864..0000000000 --- a/src/Umbraco.Core/Models/UserTourStatus.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System.Runtime.Serialization; - -namespace Umbraco.Cms.Core.Models; - -/// -/// A model representing the tours a user has taken/completed -/// -[DataContract(Name = "userTourStatus", Namespace = "")] -public class UserTourStatus : IEquatable -{ - /// - /// The tour alias - /// - [DataMember(Name = "alias")] - public string Alias { get; set; } = string.Empty; - - /// - /// If the tour is completed - /// - [DataMember(Name = "completed")] - public bool Completed { get; set; } - - /// - /// If the tour is disabled - /// - [DataMember(Name = "disabled")] - public bool Disabled { get; set; } - - public static bool operator ==(UserTourStatus? left, UserTourStatus? right) => Equals(left, right); - - public bool Equals(UserTourStatus? other) - { - if (ReferenceEquals(null, other)) - { - return false; - } - - if (ReferenceEquals(this, other)) - { - return true; - } - - return string.Equals(Alias, other.Alias); - } - - public override bool Equals(object? obj) - { - if (ReferenceEquals(null, obj)) - { - return false; - } - - if (ReferenceEquals(this, obj)) - { - return true; - } - - if (obj.GetType() != GetType()) - { - return false; - } - - return Equals((UserTourStatus)obj); - } - - public override int GetHashCode() => Alias.GetHashCode(); - - public static bool operator !=(UserTourStatus? left, UserTourStatus? right) => !Equals(left, right); -} diff --git a/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs b/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs index bd9ac9e601..145c84a0d9 100644 --- a/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs +++ b/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs @@ -53,6 +53,7 @@ public static partial class Constants public const string User2UserGroup = TableNamePrefix + "User2UserGroup"; public const string User2NodeNotify = TableNamePrefix + "User2NodeNotify"; public const string UserGroup2App = TableNamePrefix + "UserGroup2App"; + public const string UserData = TableNamePrefix + "UserData"; [Obsolete("Will be removed in Umbraco 18 as this table haven't existed since Umbraco 14.")] public const string UserGroup2Node = TableNamePrefix + "UserGroup2Node"; diff --git a/src/Umbraco.Core/Persistence/Querying/IUserDataFilter.cs b/src/Umbraco.Core/Persistence/Querying/IUserDataFilter.cs new file mode 100644 index 0000000000..fbcd7aa5d5 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Querying/IUserDataFilter.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Cms.Infrastructure.Persistence.Querying; + +public interface IUserDataFilter +{ + public ICollection? UserKeys { get; set; } + + public ICollection? Groups { get; set; } + + public ICollection? Identifiers { get; set; } +} diff --git a/src/Umbraco.Core/Persistence/Repositories/IUserDataRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IUserDataRepository.cs new file mode 100644 index 0000000000..cf53f049b7 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/IUserDataRepository.cs @@ -0,0 +1,18 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Infrastructure.Persistence.Querying; + +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IUserDataRepository +{ + Task GetAsync(Guid key); + + Task> GetAsync(int skip, int take, IUserDataFilter? filter = null); + + Task Save(IUserData userData); + + Task Update(IUserData userData); + + Task Delete(IUserData userData); +} diff --git a/src/Umbraco.Core/Services/ITourService.cs b/src/Umbraco.Core/Services/ITourService.cs deleted file mode 100644 index 568b69a961..0000000000 --- a/src/Umbraco.Core/Services/ITourService.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Services.OperationStatus; - -namespace Umbraco.Cms.Core.Services; - -public interface ITourService -{ - /// - /// Persists a for a user. - /// - /// The status to persist. - /// The key of the user to persist it for. - /// An operation status specifying if the operation was successful. - Task SetAsync(UserTourStatus status, Guid userKey); - - /// - /// Gets all for a user. - /// - /// The key of the user to get tour data for. - /// An attempt containing an enumerable of and a status. - Task, TourOperationStatus>> GetAllAsync(Guid userKey); -} diff --git a/src/Umbraco.Core/Services/IUserDataService.cs b/src/Umbraco.Core/Services/IUserDataService.cs index d0eb9a4df6..db4e01f220 100644 --- a/src/Umbraco.Core/Services/IUserDataService.cs +++ b/src/Umbraco.Core/Services/IUserDataService.cs @@ -1,10 +1,22 @@ using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Infrastructure.Persistence.Querying; namespace Umbraco.Cms.Core.Services; -[Obsolete($"Use {nameof(ISystemTroubleshootingInformationService)} instead. Will be removed in V16.")] public interface IUserDataService { - [Obsolete($"Use {nameof(ISystemTroubleshootingInformationService)} instead. Will be removed in V16.")] - IEnumerable GetUserData(); + public Task GetAsync(Guid key); + + public Task> GetAsync( + int skip, + int take, + IUserDataFilter? filter = null); + + public Task> CreateAsync(IUserData userData); + + public Task> UpdateAsync(IUserData userData); + + public Task> DeleteAsync(Guid key); } diff --git a/src/Umbraco.Core/Services/OperationStatus/TourOperationStatus.cs b/src/Umbraco.Core/Services/OperationStatus/TourOperationStatus.cs deleted file mode 100644 index 522dd8d8e0..0000000000 --- a/src/Umbraco.Core/Services/OperationStatus/TourOperationStatus.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Umbraco.Cms.Core.Services.OperationStatus; - -public enum TourOperationStatus -{ - Success, - UserNotFound, -} diff --git a/src/Umbraco.Core/Services/OperationStatus/UserDataOperationStatus.cs b/src/Umbraco.Core/Services/OperationStatus/UserDataOperationStatus.cs new file mode 100644 index 0000000000..9a05d35ea4 --- /dev/null +++ b/src/Umbraco.Core/Services/OperationStatus/UserDataOperationStatus.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Cms.Core.Services.OperationStatus; + +// FIXME: Move all authorization statuses to +public enum UserDataOperationStatus +{ + Success, + NotFound, + UserNotFound, + AlreadyExists +} diff --git a/src/Umbraco.Core/Services/TourService.cs b/src/Umbraco.Core/Services/TourService.cs deleted file mode 100644 index fd86864f39..0000000000 --- a/src/Umbraco.Core/Services/TourService.cs +++ /dev/null @@ -1,88 +0,0 @@ -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Models.Membership; -using Umbraco.Cms.Core.Serialization; -using Umbraco.Cms.Core.Services.OperationStatus; - -namespace Umbraco.Cms.Core.Services; - - -/** - * TODO: This implementation is not the greatest, - * ideally we should store tour information in its own table - * making it its own feature, instead of an ad-hoc "add-on" to users. - * additionally we should probably not store it as a JSON blob, but instead as a proper table. - * For now we'll keep doing the deserialize/serialize dance here, - * because there is no reason to spend cycles to deserialize/serialize the tour data every time we fetch/save a user. - */ -public class TourService : ITourService -{ - private readonly IJsonSerializer _jsonSerializer; - private readonly IUserService _userService; - - public TourService( - IJsonSerializer jsonSerializer, - IUserService userService) - { - _jsonSerializer = jsonSerializer; - _userService = userService; - } - - /// - public async Task SetAsync(UserTourStatus status, Guid userKey) - { - IUser? user = await _userService.GetAsync(userKey); - - if (user is null) - { - return TourOperationStatus.UserNotFound; - } - - // If the user currently have no tour data, we can just add the data and save it. - if (string.IsNullOrWhiteSpace(user.TourData)) - { - List tours = new() { status }; - user.TourData = _jsonSerializer.Serialize(tours); - _userService.Save(user); - - return TourOperationStatus.Success; - } - - // Otherwise we have to check it it already exists, and if so, replace it. - List existingTours = - _jsonSerializer.Deserialize>(user.TourData)?.ToList() ?? new List(); - UserTourStatus? found = existingTours.FirstOrDefault(x => x.Alias == status.Alias); - - if (found is not null) - { - existingTours.Remove(found); - } - - existingTours.Add(status); - - user.TourData = _jsonSerializer.Serialize(existingTours); - _userService.Save(user); - return TourOperationStatus.Success; - } - - /// - public async Task, TourOperationStatus>> GetAllAsync(Guid userKey) - { - IUser? user = await _userService.GetAsync(userKey); - - if (user is null) - { - return Attempt.FailWithStatus(TourOperationStatus.UserNotFound, Enumerable.Empty()); - } - - // No tour data, we'll just return empty. - if (string.IsNullOrWhiteSpace(user.TourData)) - { - return Attempt.SucceedWithStatus(TourOperationStatus.Success, Enumerable.Empty()); - } - - IEnumerable tours = _jsonSerializer.Deserialize>(user.TourData) - ?? Enumerable.Empty(); - - return Attempt.SucceedWithStatus(TourOperationStatus.Success, tours); - } -} diff --git a/src/Umbraco.Core/Services/UserDataService.cs b/src/Umbraco.Core/Services/UserDataService.cs index 3b6c8092a1..ff12d36030 100644 --- a/src/Umbraco.Core/Services/UserDataService.cs +++ b/src/Umbraco.Core/Services/UserDataService.cs @@ -1,38 +1,102 @@ -using System.Diagnostics; -using System.Runtime.InteropServices; -using Microsoft.Extensions.DependencyInjection; -using Umbraco.Cms.Core.Configuration; -using Umbraco.Cms.Core.DependencyInjection; +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Infrastructure.Persistence.Querying; namespace Umbraco.Cms.Core.Services; -[Obsolete($"Use {nameof(ISystemTroubleshootingInformationService)} instead. Will be removed in V16.")] -public class UserDataService : IUserDataService +public class UserDataService : RepositoryService, IUserDataService { - private readonly ISystemTroubleshootingInformationService _systemTroubleshootingInformationService; + private readonly IUserDataRepository _userDataRepository; + private readonly IUserService _userService; - [Obsolete($"Use the constructor that accepts {nameof(ISystemTroubleshootingInformationService)}")] - public UserDataService(IUmbracoVersion version, ILocalizationService localizationService) - : this(version, localizationService, StaticServiceProvider.Instance.GetRequiredService()) + public UserDataService( + ICoreScopeProvider provider, + ILoggerFactory loggerFactory, + IEventMessagesFactory eventMessagesFactory, + IUserDataRepository userDataRepository, + IUserService userService) + : base(provider, loggerFactory, eventMessagesFactory) { + _userDataRepository = userDataRepository; + _userService = userService; } - public UserDataService(IUmbracoVersion version, ILocalizationService localizationService, ISystemTroubleshootingInformationService systemTroubleshootingInformationService) - => _systemTroubleshootingInformationService = systemTroubleshootingInformationService; - - [Obsolete($"Use {nameof(ISystemTroubleshootingInformationService)} instead. Will be removed in V16.")] - public IEnumerable GetUserData() => - _systemTroubleshootingInformationService.GetTroubleshootingInformation().Select(kvp => new UserData(kvp.Key, kvp.Value)).ToArray(); - - public bool IsRunningInProcessIIS() + public async Task GetAsync(Guid key) { - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + using ICoreScope scope = ScopeProvider.CreateCoreScope(); + IUserData? userData = await _userDataRepository.GetAsync(key); + scope.Complete(); + return userData; + } + + public async Task> GetAsync(int skip, int take, IUserDataFilter? filter = null) + { + using ICoreScope scope = ScopeProvider.CreateCoreScope(); + PagedModel pagedUserData = await _userDataRepository.GetAsync(skip, take, filter); + scope.Complete(); + return pagedUserData; + } + + public async Task> CreateAsync(IUserData userData) + { + using ICoreScope scope = ScopeProvider.CreateCoreScope(); + IUserData? existingUserData = await _userDataRepository.GetAsync(userData.Key); + if (existingUserData is not null) { - return false; + return Attempt.Fail(UserDataOperationStatus.AlreadyExists, userData); } - var processName = Path.GetFileNameWithoutExtension(Process.GetCurrentProcess().ProcessName); - return processName.Contains("w3wp") || processName.Contains("iisexpress"); + if (await ReferencedUserExits(userData) is false) + { + return Attempt.Fail(UserDataOperationStatus.UserNotFound, userData); + } + + await _userDataRepository.Save(userData); + + scope.Complete(); + return Attempt.Succeed(UserDataOperationStatus.Success, userData); } + + public async Task> UpdateAsync(IUserData userData) + { + using ICoreScope scope = ScopeProvider.CreateCoreScope(); + IUserData? existingUserData = await _userDataRepository.GetAsync(userData.Key); + if (existingUserData is null) + { + return Attempt.Fail(UserDataOperationStatus.NotFound, userData); + } + + if (await ReferencedUserExits(userData) is false) + { + return Attempt.Fail(UserDataOperationStatus.UserNotFound, userData); + } + + await _userDataRepository.Update(userData); + + scope.Complete(); + return Attempt.Succeed(UserDataOperationStatus.Success, userData); + } + + public async Task> DeleteAsync(Guid key) + { + using ICoreScope scope = ScopeProvider.CreateCoreScope(); + IUserData? existingUserData = await _userDataRepository.GetAsync(key); + if (existingUserData is null) + { + return Attempt.Fail(UserDataOperationStatus.NotFound); + } + + await _userDataRepository.Delete(existingUserData); + + scope.Complete(); + return Attempt.Succeed(UserDataOperationStatus.Success); + } + + private async Task ReferencedUserExits(IUserData userData) + => await _userService.GetAsync(userData.UserKey) is not null; } diff --git a/src/Umbraco.Core/Tour/BackOfficeTourFilter.cs b/src/Umbraco.Core/Tour/BackOfficeTourFilter.cs deleted file mode 100644 index d1d8384502..0000000000 --- a/src/Umbraco.Core/Tour/BackOfficeTourFilter.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System.Text.RegularExpressions; - -namespace Umbraco.Cms.Core.Tour; - -/// -/// Represents a back-office tour filter. -/// -public class BackOfficeTourFilter -{ - /// - /// Initializes a new instance of the class. - /// - /// Value to filter out tours by a plugin, can be null - /// Value to filter out a tour file, can be null - /// Value to filter out a tour alias, can be null - /// - /// Depending on what is null will depend on how the filter is applied. - /// If pluginName is not NULL and it's matched then we check if tourFileName is not NULL and it's matched then we check - /// tour alias is not NULL and then match it, - /// if any steps is NULL then the filters upstream are applied. - /// Example, pluginName = "hello", tourFileName="stuff", tourAlias=NULL = we will filter out the tour file "stuff" from - /// the plugin "hello" but not from other plugins if the same file name exists. - /// Example, tourAlias="test.*" = we will filter out all tour aliases that start with the word "test" regardless of the - /// plugin or file name - /// - public BackOfficeTourFilter(Regex? pluginName, Regex? tourFileName, Regex? tourAlias) - { - PluginName = pluginName; - TourFileName = tourFileName; - TourAlias = tourAlias; - } - - /// - /// Gets the plugin name filtering regex. - /// - public Regex? PluginName { get; } - - /// - /// Gets the tour filename filtering regex. - /// - public Regex? TourFileName { get; } - - /// - /// Gets the tour alias filtering regex. - /// - public Regex? TourAlias { get; } - - /// - /// Creates a filter to filter on the plugin name. - /// - public static BackOfficeTourFilter FilterPlugin(Regex pluginName) - => new(pluginName, null, null); - - /// - /// Creates a filter to filter on the tour filename. - /// - public static BackOfficeTourFilter FilterFile(Regex tourFileName) - => new(null, tourFileName, null); - - /// - /// Creates a filter to filter on the tour alias. - /// - public static BackOfficeTourFilter FilterAlias(Regex tourAlias) - => new(null, null, tourAlias); -} diff --git a/src/Umbraco.Core/Tour/TourFilterCollection.cs b/src/Umbraco.Core/Tour/TourFilterCollection.cs deleted file mode 100644 index 44905f9127..0000000000 --- a/src/Umbraco.Core/Tour/TourFilterCollection.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Umbraco.Cms.Core.Composing; - -namespace Umbraco.Cms.Core.Tour; - -/// -/// Represents a collection of items. -/// -public class TourFilterCollection : BuilderCollectionBase -{ - public TourFilterCollection(Func> items) - : base(items) - { - } -} diff --git a/src/Umbraco.Core/Tour/TourFilterCollectionBuilder.cs b/src/Umbraco.Core/Tour/TourFilterCollectionBuilder.cs deleted file mode 100644 index b39bcede46..0000000000 --- a/src/Umbraco.Core/Tour/TourFilterCollectionBuilder.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.Text.RegularExpressions; -using Umbraco.Cms.Core.Composing; -using Umbraco.Extensions; - -namespace Umbraco.Cms.Core.Tour; - -/// -/// Builds a collection of items. -/// -public class TourFilterCollectionBuilder : CollectionBuilderBase -{ - private readonly HashSet _instances = new(); - - /// - /// Adds a filter instance. - /// - public void AddFilter(BackOfficeTourFilter filter) => _instances.Add(filter); - - /// - protected override IEnumerable CreateItems(IServiceProvider factory) => - base.CreateItems(factory).Concat(_instances); - - /// - /// Removes a filter instance. - /// - public void RemoveFilter(BackOfficeTourFilter filter) => _instances.Remove(filter); - - /// - /// Removes all filter instances. - /// - public void RemoveAllFilters() => _instances.Clear(); - - /// - /// Removes filters matching a condition. - /// - public void RemoveFilter(Func predicate) => - _instances.RemoveWhere(new Predicate(predicate)); - - /// - /// Creates and adds a filter instance filtering by plugin name. - /// - public void AddFilterByPlugin(string pluginName) - { - pluginName = pluginName.EnsureStartsWith("^").EnsureEndsWith("$"); - _instances.Add(BackOfficeTourFilter.FilterPlugin(new Regex(pluginName, RegexOptions.IgnoreCase))); - } - - /// - /// Creates and adds a filter instance filtering by tour filename. - /// - public void AddFilterByFile(string filename) - { - filename = filename.EnsureStartsWith("^").EnsureEndsWith("$"); - _instances.Add(BackOfficeTourFilter.FilterFile(new Regex(filename, RegexOptions.IgnoreCase))); - } -} diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs index 64e8af2290..f62227ddba 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs @@ -78,6 +78,7 @@ public static partial class UmbracoBuilderExtensions builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); + builder.Services.AddUnique(); return builder; } diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs index 23ddc6ce96..a16e16f535 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs @@ -87,6 +87,7 @@ public class DatabaseSchemaCreator typeof(Webhook2HeadersDto), typeof(WebhookLogDto), typeof(WebhookRequestDto), + typeof(UserDataDto), }; private readonly IUmbracoDatabase _database; diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs index 204a09a609..d459bfaf63 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs @@ -77,5 +77,6 @@ public class UmbracoPlan : MigrationPlan To("{C567DE81-DF92-4B99-BEA8-CD34EF99DA5D}"); To("{0D82C836-96DD-480D-A924-7964E458BD34}"); To("{1A0FBC8A-6FC6-456C-805C-B94816B2E570}"); + To("{302DE171-6D83-4B6B-B3C0-AC8808A16CA1}"); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/AddGuidsToUsers.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/AddGuidsToUsers.cs index 32cf861d19..4a2f34d149 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/AddGuidsToUsers.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/AddGuidsToUsers.cs @@ -45,13 +45,13 @@ internal class AddGuidsToUsers : UnscopedMigrationBase { var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); AddColumnIfNotExists(columns, NewColumnName); - List? userDtos = Database.Fetch(); + List? userDtos = Database.Fetch(); if (userDtos is null) { return; } - UserDto? superUser = userDtos.FirstOrDefault(x => x.Id == -1); + NewUserDto? superUser = userDtos.FirstOrDefault(x => x.Id == -1); if (superUser is not null) { superUser.Key = Constants.Security.SuperUserKey; @@ -83,7 +83,7 @@ internal class AddGuidsToUsers : UnscopedMigrationBase Database.Execute("PRAGMA foreign_keys=off;"); Database.Execute("BEGIN TRANSACTION;"); - List users = Database.Fetch().Select(x => new UserDto + List users = Database.Fetch().Select(x => new NewUserDto { Id = x.Id, Key = x.Id is -1 ? Constants.Security.SuperUserKey : Guid.NewGuid(), @@ -109,9 +109,9 @@ internal class AddGuidsToUsers : UnscopedMigrationBase }).ToList(); Delete.Table(Constants.DatabaseSchema.Tables.User).Do(); - Create.Table().Do(); + Create.Table().Do(); - foreach (UserDto user in users) + foreach (NewUserDto user in users) { Database.Insert(Constants.DatabaseSchema.Tables.User, "id", false, user); } @@ -120,7 +120,7 @@ internal class AddGuidsToUsers : UnscopedMigrationBase MigrateTwoFactorLogins(users); } - private void MigrateExternalLogins(List userDtos) + private void MigrateExternalLogins(List userDtos) { List? externalLogins = Database.Fetch(); if (externalLogins is null) @@ -130,7 +130,7 @@ internal class AddGuidsToUsers : UnscopedMigrationBase foreach (ExternalLoginDto externalLogin in externalLogins) { - UserDto? associatedUser = userDtos.FirstOrDefault(x => x.Id.ToGuid() == externalLogin.UserOrMemberKey); + NewUserDto? associatedUser = userDtos.FirstOrDefault(x => x.Id.ToGuid() == externalLogin.UserOrMemberKey); if (associatedUser is null) { continue; @@ -141,7 +141,7 @@ internal class AddGuidsToUsers : UnscopedMigrationBase } } - private void MigrateTwoFactorLogins(List userDtos) + private void MigrateTwoFactorLogins(List userDtos) { // TODO: TEST ME! List? twoFactorLoginDtos = Database.Fetch(); @@ -152,7 +152,7 @@ internal class AddGuidsToUsers : UnscopedMigrationBase foreach (TwoFactorLoginDto twoFactorLoginDto in twoFactorLoginDtos) { - UserDto? associatedUser = userDtos.FirstOrDefault(x => x.Id.ToGuid() == twoFactorLoginDto.UserOrMemberKey); + NewUserDto? associatedUser = userDtos.FirstOrDefault(x => x.Id.ToGuid() == twoFactorLoginDto.UserOrMemberKey); if (associatedUser is null) { @@ -276,4 +276,123 @@ internal class AddGuidsToUsers : UnscopedMigrationBase [Reference(ReferenceType.Many, ReferenceMemberName = "UserId")] public HashSet UserStartNodeDtos { get; set; } } + + [TableName(TableName)] + [PrimaryKey("id", AutoIncrement = true)] + [ExplicitColumns] + public class NewUserDto + { + public const string TableName = Constants.DatabaseSchema.Tables.User; + + public NewUserDto() + { + UserGroupDtos = new List(); + UserStartNodeDtos = new HashSet(); + } + + [Column("id")] + [PrimaryKeyColumn(Name = "PK_user")] + public int Id { get; set; } + + [Column("userDisabled")] + [Constraint(Default = "0")] + public bool Disabled { get; set; } + + [Column("key")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Constraint(Default = SystemMethods.NewGuid)] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoUser_userKey")] + public Guid Key { get; set; } + + [Column("userNoConsole")] + [Constraint(Default = "0")] + public bool NoConsole { get; set; } + + [Column("userName")] public string UserName { get; set; } = null!; + + [Column("userLogin")] + [Length(125)] + [Index(IndexTypes.NonClustered)] + public string? Login { get; set; } + + [Column("userPassword")] [Length(500)] public string? Password { get; set; } + + /// + /// This will represent a JSON structure of how the password has been created (i.e hash algorithm, iterations) + /// + [Column("passwordConfig")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(500)] + public string? PasswordConfig { get; set; } + + [Column("userEmail")] public string Email { get; set; } = null!; + + [Column("userLanguage")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(10)] + public string? UserLanguage { get; set; } + + [Column("securityStampToken")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(255)] + public string? SecurityStampToken { get; set; } + + [Column("failedLoginAttempts")] + [NullSetting(NullSetting = NullSettings.Null)] + public int? FailedLoginAttempts { get; set; } + + [Column("lastLockoutDate")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? LastLockoutDate { get; set; } + + [Column("lastPasswordChangeDate")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? LastPasswordChangeDate { get; set; } + + [Column("lastLoginDate")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? LastLoginDate { get; set; } + + [Column("emailConfirmedDate")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? EmailConfirmedDate { get; set; } + + [Column("invitedDate")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? InvitedDate { get; set; } + + [Column("createDate")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime CreateDate { get; set; } = DateTime.Now; + + [Column("updateDate")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime UpdateDate { get; set; } = DateTime.Now; + + /// + /// Will hold the media file system relative path of the users custom avatar if they uploaded one + /// + [Column("avatar")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(500)] + public string? Avatar { get; set; } + + /// + /// A Json blob stored for recording tour data for a user + /// + [Column("tourData")] + [NullSetting(NullSetting = NullSettings.Null)] + [SpecialDbType(SpecialDbTypes.NVARCHARMAX)] + public string? TourData { get; set; } + + [ResultColumn] + [Reference(ReferenceType.Many, ReferenceMemberName = "UserId")] + public List UserGroupDtos { get; set; } + + [ResultColumn] + [Reference(ReferenceType.Many, ReferenceMemberName = "UserId")] + public HashSet UserStartNodeDtos { get; set; } + } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/MigrateTours.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/MigrateTours.cs new file mode 100644 index 0000000000..fedfd43c51 --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/MigrateTours.cs @@ -0,0 +1,278 @@ +using System.Runtime.Serialization; +using NPoco; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_14_0_0; + +internal class MigrateTours : UnscopedMigrationBase +{ + private readonly IJsonSerializer _jsonSerializer; + private readonly IScopeProvider _scopeProvider; + + public MigrateTours( + IMigrationContext context, + IJsonSerializer jsonSerializer, + IScopeProvider scopeProvider) + : base(context) + { + _jsonSerializer = jsonSerializer; + _scopeProvider = scopeProvider; + } + + protected override void Migrate() + { + using IScope scope = _scopeProvider.CreateScope(); + using IDisposable notificationSuppression = scope.Notifications.Suppress(); + ScopeDatabase(scope); + + // create table + if (TableExists(Constants.DatabaseSchema.Tables.UserData)) + { + return; + } + + Create.Table().Do(); + + // transform all existing UserTour fields in to userdata + List? users = Database.Fetch(); + List userData = new List(); + foreach (OldUserDto user in users) + { + if (user.TourData is null) + { + continue; + } + + TourData[]? tourData = _jsonSerializer.Deserialize(user.TourData); + if (tourData is null) + { + // invalid value + continue; + } + + foreach (TourData data in tourData) + { + var userDataFromTour = new UserDataDto + { + Key = Guid.NewGuid(), + UserKey = user.Key, + Group = "umbraco.tours", + Identifier = data.Alias, + Value = _jsonSerializer.Serialize(new TourValue + { + Completed = data.Completed, + Disabled = data.Disabled, + }), + }; + userData.Add(userDataFromTour); + } + } + + Database.InsertBulk(userData); + + // remove old column + if (DatabaseType != DatabaseType.SQLite) + { + MigrateUserTableSqlServer(); + scope.Complete(); + return; + } + + MigrateUserTableSqlite(); + scope.Complete(); + } + + private void MigrateUserTableSqlite() + { + /* + * We commit the initial transaction started by the scope. This is required in order to disable the foreign keys. + * We then begin a new transaction, this transaction will be committed or rolled back by the scope, like normal. + * We don't have to worry about re-enabling the foreign keys, since these are enabled by default every time a connection is established. + * + * Ideally we'd want to do this with the unscoped database we get, however, this cannot be done, + * since our scoped database cannot share a connection with the unscoped database, so a new one will be created, which enables the foreign keys. + * Similarly we cannot use Database.CompleteTransaction(); since this also closes the connection, + * so starting a new transaction would re-enable foreign keys. + */ + Database.Execute("COMMIT;"); + Database.Execute("PRAGMA foreign_keys=off;"); + Database.Execute("BEGIN TRANSACTION;"); + + List userDtos = Database.Fetch(); + + Delete.Table(Constants.DatabaseSchema.Tables.User).Do(); + Create.Table().Do(); + + // We have to insert one at a time to be able to not auto increment the id. + foreach (UserDto user in userDtos) + { + Database.Insert(Constants.DatabaseSchema.Tables.User, "id", false, user); + } + } + + private void MigrateUserTableSqlServer() + { + Delete.Column("tourData").FromTable(Constants.DatabaseSchema.Tables.User).Do(); + } + + + [TableName(TableName)] + [PrimaryKey("id", AutoIncrement = true)] + [ExplicitColumns] + public class OldUserDto + { + public const string TableName = Constants.DatabaseSchema.Tables.User; + + public OldUserDto() + { + UserGroupDtos = new List(); + UserStartNodeDtos = new HashSet(); + } + + [Column("id")] + [PrimaryKeyColumn(Name = "PK_user")] + public int Id { get; set; } + + [Column("userDisabled")] + [Constraint(Default = "0")] + public bool Disabled { get; set; } + + [Column("key")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Constraint(Default = SystemMethods.NewGuid)] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoUser_userKey")] + public Guid Key { get; set; } + + [Column("userNoConsole")] + [Constraint(Default = "0")] + public bool NoConsole { get; set; } + + [Column("userName")] public string UserName { get; set; } = null!; + + [Column("userLogin")] + [Length(125)] + [Index(IndexTypes.NonClustered)] + public string? Login { get; set; } + + [Column("userPassword")] [Length(500)] public string? Password { get; set; } + + /// + /// This will represent a JSON structure of how the password has been created (i.e hash algorithm, iterations) + /// + [Column("passwordConfig")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(500)] + public string? PasswordConfig { get; set; } + + [Column("userEmail")] public string Email { get; set; } = null!; + + [Column("userLanguage")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(10)] + public string? UserLanguage { get; set; } + + [Column("securityStampToken")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(255)] + public string? SecurityStampToken { get; set; } + + [Column("failedLoginAttempts")] + [NullSetting(NullSetting = NullSettings.Null)] + public int? FailedLoginAttempts { get; set; } + + [Column("lastLockoutDate")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? LastLockoutDate { get; set; } + + [Column("lastPasswordChangeDate")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? LastPasswordChangeDate { get; set; } + + [Column("lastLoginDate")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? LastLoginDate { get; set; } + + [Column("emailConfirmedDate")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? EmailConfirmedDate { get; set; } + + [Column("invitedDate")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? InvitedDate { get; set; } + + [Column("createDate")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime CreateDate { get; set; } = DateTime.Now; + + [Column("updateDate")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime UpdateDate { get; set; } = DateTime.Now; + + /// + /// Will hold the media file system relative path of the users custom avatar if they uploaded one + /// + [Column("avatar")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(500)] + public string? Avatar { get; set; } + + /// + /// A Json blob stored for recording tour data for a user + /// + [Column("tourData")] + [NullSetting(NullSetting = NullSettings.Null)] + [SpecialDbType(SpecialDbTypes.NVARCHARMAX)] + public string? TourData { get; set; } + + [ResultColumn] + [Reference(ReferenceType.Many, ReferenceMemberName = "UserId")] + public List UserGroupDtos { get; set; } + + [ResultColumn] + [Reference(ReferenceType.Many, ReferenceMemberName = "UserId")] + public HashSet UserStartNodeDtos { get; set; } + } + + public class TourData() + { + /// + /// The tour alias + /// + [DataMember(Name = "alias")] + public string Alias { get; set; } = string.Empty; + + /// + /// If the tour is completed + /// + [DataMember(Name = "completed")] + public bool Completed { get; set; } + + /// + /// If the tour is disabled + /// + [DataMember(Name = "disabled")] + public bool Disabled { get; set; } + } + + public class TourValue() + { + /// + /// If the tour is completed + /// + [DataMember(Name = "completed")] + public bool Completed { get; set; } + + /// + /// If the tour is disabled + /// + [DataMember(Name = "disabled")] + public bool Disabled { get; set; } + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/UserDataDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/UserDataDto.cs new file mode 100644 index 0000000000..d04da681cf --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/UserDataDto.cs @@ -0,0 +1,37 @@ +using NPoco; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; + +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("key", AutoIncrement = false)] +[ExplicitColumns] +public class UserDataDto +{ + public const string TableName = Constants.DatabaseSchema.Tables.UserData; + + [Column("key")] + [PrimaryKeyColumn(Name = "PK_umbracoUserDataDto", AutoIncrement = false)] + public Guid Key { get; set; } + + [Column("userKey")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Index(IndexTypes.NonClustered, Name = "IX_umbracoUserDataDto_UserKey_Group_Identifier", IncludeColumns = "group,identifier")] + [ForeignKey(typeof(UserDto), Column = "key")] + public Guid UserKey { get; set; } + + [Column("group")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public required string Group { get; set; } + + [Column("identifier")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public required string Identifier { get; set; } + + [Column("value")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [SpecialDbType(SpecialDbTypes.NVARCHARMAX)] + public required string Value { get; set; } +} diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/UserDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/UserDto.cs index f71a8bc60d..012e446162 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/UserDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/UserDto.cs @@ -111,14 +111,6 @@ public class UserDto [Length(500)] public string? Avatar { get; set; } - /// - /// A Json blob stored for recording tour data for a user - /// - [Column("tourData")] - [NullSetting(NullSetting = NullSettings.Null)] - [SpecialDbType(SpecialDbTypes.NVARCHARMAX)] - public string? TourData { get; set; } - [ResultColumn] [Reference(ReferenceType.Many, ReferenceMemberName = "UserId")] public List UserGroupDtos { get; set; } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/UserFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/UserFactory.cs index e8eb1005a1..969775da10 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/UserFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/UserFactory.cs @@ -47,7 +47,6 @@ internal static class UserFactory user.Avatar = dto.Avatar; user.EmailConfirmedDate = dto.EmailConfirmedDate; user.InvitedDate = dto.InvitedDate; - user.TourData = dto.TourData; // reset dirty initial properties (U4-1946) user.ResetDirtyProperties(false); @@ -83,7 +82,6 @@ internal static class UserFactory Avatar = entity.Avatar, EmailConfirmedDate = entity.EmailConfirmedDate, InvitedDate = entity.InvitedDate, - TourData = entity.TourData, }; if (entity.StartContentIds is not null) diff --git a/src/Umbraco.Infrastructure/Persistence/Querying/UserDataFilter.cs b/src/Umbraco.Infrastructure/Persistence/Querying/UserDataFilter.cs new file mode 100644 index 0000000000..6b641bcb32 --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Querying/UserDataFilter.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Cms.Infrastructure.Persistence.Querying; + +public class UserDataFilter : IUserDataFilter +{ + public ICollection? UserKeys { get; set; } + + public ICollection? Groups { get; set; } + + public ICollection? Identifiers { get; set; } +} diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserDataRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserDataRepository.cs new file mode 100644 index 0000000000..85bcceb698 --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserDataRepository.cs @@ -0,0 +1,120 @@ +using NPoco; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Persistence.Querying; +using Umbraco.Cms.Infrastructure.Scoping; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class UserDataRepository : IUserDataRepository +{ + private readonly IScopeAccessor _scopeAccessor; + + public UserDataRepository(IScopeAccessor scopeAccessor) => _scopeAccessor = scopeAccessor; + + public async Task GetAsync(Guid key) + { + Sql? sql = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql() + .Select() + .From() + .Where(dataDto => dataDto.Key == key) + .OrderBy(dto => dto.Identifier); // need to order to skiptake; + + UserDataDto? dto = await _scopeAccessor.AmbientScope?.Database.FirstOrDefaultAsync(sql)!; + + return dto is null ? null : Map(dto); + } + + public async Task> GetAsync(int skip, int take, IUserDataFilter? filter = null) + { + Sql? sql = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql() + .Select() + .From(); + + if (sql is null) + { + return new PagedModel(); + } + + if (filter is not null) + { + sql = ApplyFilter(sql, filter); + } + + sql = sql.OrderBy(dto => dto.Identifier); // need to order to skiptake + + List? userDataDtos = + await _scopeAccessor.AmbientScope?.Database.SkipTakeAsync(skip, take, sql)!; + + var totalItems = _scopeAccessor.AmbientScope?.Database.Count(sql!) ?? 0; + + return new PagedModel { Total = totalItems, Items = DtosToModels(userDataDtos) }; + } + + public async Task Save(IUserData userData) + { + await _scopeAccessor.AmbientScope?.Database.InsertAsync(Map(userData))!; + return userData; + } + + public async Task Update(IUserData userData) + { + await _scopeAccessor.AmbientScope?.Database.UpdateAsync(Map(userData))!; + return userData; + } + + public async Task Delete(IUserData userData) + { + Sql sql = _scopeAccessor.AmbientScope!.Database.SqlContext.Sql() + .Delete() + .Where(dto => dto.Key == userData.Key); + + await _scopeAccessor.AmbientScope?.Database.ExecuteAsync(sql)!; + } + + private Sql ApplyFilter(Sql sql, IUserDataFilter filter) + { + if (filter.Groups?.Count > 0) + { + sql = sql.Where(dto => filter.Groups.Contains(dto.Group)); + } + + if (filter.Identifiers?.Count > 0) + { + sql = sql.Where(dto => filter.Identifiers.Contains(dto.Identifier)); + } + + if (filter.UserKeys?.Count > 0) + { + sql = sql.Where(dto => filter.UserKeys.Contains(dto.UserKey)); + } + + return sql; + } + + private IEnumerable DtosToModels(IEnumerable dtos) + => dtos.Select(Map); + + private IUserData Map(UserDataDto dto) + => new UserData + { + Key = dto.Key, + Group = dto.Group, + Identifier = dto.Identifier, + Value = dto.Value, + UserKey = dto.UserKey, + }; + + private UserDataDto Map(IUserData userData) + => new() + { + Key = userData.Key, + Group = userData.Group, + Identifier = userData.Identifier, + Value = userData.Value, + UserKey = userData.UserKey, + }; +} diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs index 0b78a30f0f..cb86f717fe 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs @@ -777,8 +777,7 @@ SELECT 4 AS [Key], COUNT(id) AS [Value] FROM umbracoUser WHERE userDisabled = 0 {"updateDate", "UpdateDate"}, {"avatar", "Avatar"}, {"emailConfirmedDate", "EmailConfirmedDate"}, - {"invitedDate", "InvitedDate"}, - {"tourData", "TourData"} + {"invitedDate", "InvitedDate"} }; // create list of properties that have changed diff --git a/src/Umbraco.Web.UI.New/umbraco/Data/Umbraco.sqlite.db b/src/Umbraco.Web.UI.New/umbraco/Data/Umbraco.sqlite.db new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tools/Umbraco.JsonSchema/UmbracoCmsSchema.cs b/tools/Umbraco.JsonSchema/UmbracoCmsSchema.cs index f708644634..f2b3bacdb9 100644 --- a/tools/Umbraco.JsonSchema/UmbracoCmsSchema.cs +++ b/tools/Umbraco.JsonSchema/UmbracoCmsSchema.cs @@ -49,8 +49,6 @@ internal class UmbracoCmsSchema public SecuritySettings Security { get; set; } = null!; - public TourSettings Tours { get; set; } = null!; - public TypeFinderSettings TypeFinder { get; set; } = null!; public WebRoutingSettings WebRouting { get; set; } = null!;