From fef793326f3a350eef19d643678ce6b33afb508c Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska <21998037+elit0451@users.noreply.github.com> Date: Mon, 19 Dec 2022 13:50:18 +0100 Subject: [PATCH] New Backoffice: Health check controller (#13543) * Adding health check controllers - getAll & getGroupByName * Adding HealthCheckGroup mapper * Adding viewModels * Adding a factory for building a health check group with result view model * Registering the mapper and factory * Updating OpenApi.json * Remove unnecessary checks - we instantiate the target before calling the Map() * Adding ProducesResponseType for GetAll() * Fixing usings * Making checks required for a health check group * Adding OK() around result to be explicit * Adding default values to skip and take * Adding Umbraco.Code comments * Removing ? from HealthCheckGroupWithResultViewModel return type * Move the grouping creation to the factory * Adding Actions[] for each health check + mapping * Defaulting ValueRequired to false * Refactoring routes - from health-check to health-check-group * Move to HealthCheckGroup folder * Fix OpenApi.json * Changing class name and making Key of HealthCheckActionViewModel non-nullable * Fixing namespace * Migrating ExecuteAction endpoint * Add execute-action endpoint to OpenApi.json * Use Task.FromResult() around the action result * Fixing namespaces and swagger group name * Fixing naming of the key in the health check action * Fix tag names in OpenApi.json --- .../ExecuteActionHealthCheckController.cs | 64 ++ .../Group/AllHealthCheckGroupController.cs | 45 ++ ...ameWithResultHealthCheckGroupController.cs | 47 ++ .../Group/HealthCheckGroupControllerBase.cs | 13 + .../HealthCheck/HealthCheckControllerBase.cs | 13 + .../FactoryBuilderExtensions.cs | 8 +- .../MappingBuilderExtensions.cs | 6 +- ...lthCheckGroupWithResultViewModelFactory.cs | 74 +++ ...lthCheckGroupWithResultViewModelFactory.cs | 13 + .../HealthCheckViewModelsMapDefinition.cs | 80 +++ .../LanguageViewModelsMapDefinition.cs | 16 +- src/Umbraco.Cms.Api.Management/OpenApi.json | 589 +++++++++++++----- .../HealthCheck/HealthCheckActionViewModel.cs | 50 ++ .../HealthCheck/HealthCheckGroupViewModel.cs | 14 + .../HealthCheckGroupWithResultViewModel.cs | 14 + .../HealthCheck/HealthCheckResultViewModel.cs | 26 + .../HealthCheck/HealthCheckViewModel.cs | 19 + .../HealthCheckWithResultViewModel.cs | 10 + src/Umbraco.Core/Constants-HealthChecks.cs | 5 + 19 files changed, 937 insertions(+), 169 deletions(-) create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/HealthCheck/ExecuteActionHealthCheckController.cs create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/HealthCheck/Group/AllHealthCheckGroupController.cs create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/HealthCheck/Group/ByNameWithResultHealthCheckGroupController.cs create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/HealthCheck/Group/HealthCheckGroupControllerBase.cs create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/HealthCheck/HealthCheckControllerBase.cs create mode 100644 src/Umbraco.Cms.Api.Management/Factories/HealthCheckGroupWithResultViewModelFactory.cs create mode 100644 src/Umbraco.Cms.Api.Management/Factories/IHealthCheckGroupWithResultViewModelFactory.cs create mode 100644 src/Umbraco.Cms.Api.Management/Mapping/HealthCheck/HealthCheckViewModelsMapDefinition.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/HealthCheck/HealthCheckActionViewModel.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/HealthCheck/HealthCheckGroupViewModel.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/HealthCheck/HealthCheckGroupWithResultViewModel.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/HealthCheck/HealthCheckResultViewModel.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/HealthCheck/HealthCheckViewModel.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/HealthCheck/HealthCheckWithResultViewModel.cs diff --git a/src/Umbraco.Cms.Api.Management/Controllers/HealthCheck/ExecuteActionHealthCheckController.cs b/src/Umbraco.Cms.Api.Management/Controllers/HealthCheck/ExecuteActionHealthCheckController.cs new file mode 100644 index 0000000000..bdfef7dc39 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/HealthCheck/ExecuteActionHealthCheckController.cs @@ -0,0 +1,64 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Api.Management.ViewModels.HealthCheck; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.HealthChecks; +using Umbraco.Cms.Core.Mapping; + +namespace Umbraco.Cms.Api.Management.Controllers.HealthCheck; + +public class ExecuteActionHealthCheckController : HealthCheckControllerBase +{ + private readonly HealthCheckCollection _healthChecks; + private readonly IUmbracoMapper _umbracoMapper; + private readonly IList _disabledCheckIds; + + public ExecuteActionHealthCheckController( + HealthCheckCollection healthChecks, + IOptions healthChecksSettings, + IUmbracoMapper umbracoMapper) + { + _healthChecks = healthChecks; + _disabledCheckIds = healthChecksSettings.Value + .DisabledChecks + .Select(x => x.Id) + .ToList(); + _umbracoMapper = umbracoMapper; + } + + /// + /// Executes a given action from a HealthCheck. + /// + /// The action to be executed. + /// The result of a health check after the health check action is performed. + [HttpPost("execute-action")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(HealthCheckResultViewModel), StatusCodes.Status200OK)] + public async Task> ExecuteAction(HealthCheckActionViewModel action) + { + Guid healthCheckKey = action.HealthCheckKey; + + Core.HealthChecks.HealthCheck? healthCheck = _healthChecks + .Where(x => _disabledCheckIds.Contains(healthCheckKey) == false) + .FirstOrDefault(x => x.Id == healthCheckKey); + + if (healthCheck is null) + { + var invalidModelProblem = new ProblemDetails + { + Title = "Health Check Not Found", + Detail = $"No health check found with key = {healthCheckKey}", + Status = StatusCodes.Status400BadRequest, + Type = "Error", + }; + + return await Task.FromResult(BadRequest(invalidModelProblem)); + } + + HealthCheckStatus result = healthCheck.ExecuteAction(_umbracoMapper.Map(action)!); + + return await Task.FromResult(Ok(_umbracoMapper.Map(result))); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/HealthCheck/Group/AllHealthCheckGroupController.cs b/src/Umbraco.Cms.Api.Management/Controllers/HealthCheck/Group/AllHealthCheckGroupController.cs new file mode 100644 index 0000000000..a4f8d91c4c --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/HealthCheck/Group/AllHealthCheckGroupController.cs @@ -0,0 +1,45 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.ViewModels.HealthCheck; +using Umbraco.Cms.Core.HealthChecks; +using Umbraco.Cms.Core.Mapping; + +namespace Umbraco.Cms.Api.Management.Controllers.HealthCheck.Group; + +public class AllHealthCheckGroupController : HealthCheckGroupControllerBase +{ + private readonly HealthCheckCollection _healthChecks; + private readonly IHealthCheckGroupWithResultViewModelFactory _healthCheckGroupWithResultViewModelFactory; + private readonly IUmbracoMapper _umbracoMapper; + + public AllHealthCheckGroupController( + HealthCheckCollection healthChecks, + IHealthCheckGroupWithResultViewModelFactory healthCheckGroupWithResultViewModelFactory, + IUmbracoMapper umbracoMapper) + { + _healthChecks = healthChecks; + _healthCheckGroupWithResultViewModelFactory = healthCheckGroupWithResultViewModelFactory; + _umbracoMapper = umbracoMapper; + } + + /// + /// Gets a paginated grouped list of all health checks without checking the result of each health check. + /// + /// The amount of items to skip. + /// The amount of items to take. + /// The paged result of health checks, grouped by health check group name. + [HttpGet] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> All(int skip = 0, int take = 100) + { + IEnumerable> groups = _healthCheckGroupWithResultViewModelFactory + .CreateGroupingFromHealthCheckCollection(_healthChecks) + .Skip(skip) + .Take(take); + + return await Task.FromResult(Ok(_umbracoMapper.Map>(groups))); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/HealthCheck/Group/ByNameWithResultHealthCheckGroupController.cs b/src/Umbraco.Cms.Api.Management/Controllers/HealthCheck/Group/ByNameWithResultHealthCheckGroupController.cs new file mode 100644 index 0000000000..14965ea926 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/HealthCheck/Group/ByNameWithResultHealthCheckGroupController.cs @@ -0,0 +1,47 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.ViewModels.HealthCheck; +using Umbraco.Cms.Core.HealthChecks; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Api.Management.Controllers.HealthCheck.Group; + +public class ByNameWithResultHealthCheckGroupController : HealthCheckGroupControllerBase +{ + private readonly HealthCheckCollection _healthChecks; + private readonly IHealthCheckGroupWithResultViewModelFactory _healthCheckGroupWithResultViewModelFactory; + + public ByNameWithResultHealthCheckGroupController( + HealthCheckCollection healthChecks, + IHealthCheckGroupWithResultViewModelFactory healthCheckGroupWithResultViewModelFactory) + { + _healthChecks = healthChecks; + _healthCheckGroupWithResultViewModelFactory = healthCheckGroupWithResultViewModelFactory; + } + + /// + /// Gets a health check group with all its health checks by a group name. + /// + /// The name of the group. + /// The health check result(s) will be included as part of the health checks. + /// The health check group or not found result. + [HttpGet("{name}")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(NotFoundResult), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(HealthCheckGroupWithResultViewModel), StatusCodes.Status200OK)] + public async Task> ByName(string name) + { + IEnumerable> groups = _healthCheckGroupWithResultViewModelFactory + .CreateGroupingFromHealthCheckCollection(_healthChecks); + + IGrouping? group = groups.FirstOrDefault(x => x.Key.InvariantEquals(name.Trim())); + + if (group is null) + { + return await Task.FromResult(NotFound()); + } + + return await Task.FromResult(Ok(_healthCheckGroupWithResultViewModelFactory.CreateHealthCheckGroupWithResultViewModel(group))); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/HealthCheck/Group/HealthCheckGroupControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/HealthCheck/Group/HealthCheckGroupControllerBase.cs new file mode 100644 index 0000000000..06731d32e1 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/HealthCheck/Group/HealthCheckGroupControllerBase.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Routing; +using Constants = Umbraco.Cms.Core.Constants; + +namespace Umbraco.Cms.Api.Management.Controllers.HealthCheck.Group; + +[ApiController] +[VersionedApiBackOfficeRoute($"{Constants.HealthChecks.RoutePath.HealthCheck}-group")] +[ApiExplorerSettings(GroupName = "Health Check")] +[ApiVersion("1.0")] +public abstract class HealthCheckGroupControllerBase : ManagementApiControllerBase +{ +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/HealthCheck/HealthCheckControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/HealthCheck/HealthCheckControllerBase.cs new file mode 100644 index 0000000000..c5681f3323 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/HealthCheck/HealthCheckControllerBase.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Routing; +using Constants = Umbraco.Cms.Core.Constants; + +namespace Umbraco.Cms.Api.Management.Controllers.HealthCheck; + +[ApiController] +[VersionedApiBackOfficeRoute($"{Constants.HealthChecks.RoutePath.HealthCheck}")] +[ApiExplorerSettings(GroupName = "Health Check")] +[ApiVersion("1.0")] +public abstract class HealthCheckControllerBase : ManagementApiControllerBase +{ +} diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/FactoryBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/FactoryBuilderExtensions.cs index 834a1cab7b..b8d04c115c 100644 --- a/src/Umbraco.Cms.Api.Management/DependencyInjection/FactoryBuilderExtensions.cs +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/FactoryBuilderExtensions.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Api.Management.Factories; using Umbraco.New.Cms.Core.Factories; @@ -9,11 +9,13 @@ public static class FactoryBuilderExtensions { internal static IUmbracoBuilder AddFactories(this IUmbracoBuilder builder) { - builder.Services.AddTransient(); - builder.Services.AddTransient(); builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); + builder.Services.AddTransient(); + return builder; } } diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/MappingBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/MappingBuilderExtensions.cs index 710a717f86..4f1d5c5842 100644 --- a/src/Umbraco.Cms.Api.Management/DependencyInjection/MappingBuilderExtensions.cs +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/MappingBuilderExtensions.cs @@ -1,7 +1,8 @@ -using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Api.Management.Mapping.Culture; using Umbraco.Cms.Api.Management.Mapping.Dictionary; +using Umbraco.Cms.Api.Management.Mapping.HealthCheck; using Umbraco.Cms.Api.Management.Mapping.Installer; using Umbraco.Cms.Api.Management.Mapping.Languages; using Umbraco.Cms.Api.Management.Mapping.Relation; @@ -21,7 +22,8 @@ public static class MappingBuilderExtensions .Add() .Add() .Add() - .Add(); + .Add() + .Add(); return builder; } diff --git a/src/Umbraco.Cms.Api.Management/Factories/HealthCheckGroupWithResultViewModelFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/HealthCheckGroupWithResultViewModelFactory.cs new file mode 100644 index 0000000000..c09dd6d5cc --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Factories/HealthCheckGroupWithResultViewModelFactory.cs @@ -0,0 +1,74 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Api.Management.ViewModels.HealthCheck; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.HealthChecks; +using Umbraco.Cms.Core.Mapping; + +namespace Umbraco.Cms.Api.Management.Factories; + +public class HealthCheckGroupWithResultViewModelFactory : IHealthCheckGroupWithResultViewModelFactory +{ + private readonly HealthChecksSettings _healthChecksSettings; + private readonly ILogger _logger; + private readonly IUmbracoMapper _umbracoMapper; + + public HealthCheckGroupWithResultViewModelFactory( + IOptions healthChecksSettings, + ILogger logger, + IUmbracoMapper umbracoMapper) + { + _healthChecksSettings = healthChecksSettings.Value; + _logger = logger; + _umbracoMapper = umbracoMapper; + } + + public IEnumerable> CreateGroupingFromHealthCheckCollection(HealthCheckCollection healthChecks) + { + IList disabledCheckIds = _healthChecksSettings.DisabledChecks + .Select(x => x.Id) + .ToList(); + + IEnumerable> groups = healthChecks + .Where(x => disabledCheckIds.Contains(x.Id) == false) + .GroupBy(x => x.Group) + .OrderBy(x => x.Key); + + return groups; + } + + public HealthCheckGroupWithResultViewModel CreateHealthCheckGroupWithResultViewModel(IGrouping healthCheckGroup) + { + var healthChecks = new List(); + + foreach (HealthCheck healthCheck in healthCheckGroup) + { + healthChecks.Add(CreateHealthCheckWithResultViewModel(healthCheck)); + } + + var healthCheckGroupViewModel = new HealthCheckGroupWithResultViewModel + { + Name = healthCheckGroup.Key, + Checks = healthChecks + }; + + return healthCheckGroupViewModel; + } + + public HealthCheckWithResultViewModel CreateHealthCheckWithResultViewModel(HealthCheck healthCheck) + { + _logger.LogDebug("Running health check: " + healthCheck.Name); + + IEnumerable results = healthCheck.GetStatus().Result; + + var healthCheckViewModel = new HealthCheckWithResultViewModel + { + Key = healthCheck.Id, + Name = healthCheck.Name, + Description = healthCheck.Description, + Results = _umbracoMapper.MapEnumerable(results) + }; + + return healthCheckViewModel; + } +} diff --git a/src/Umbraco.Cms.Api.Management/Factories/IHealthCheckGroupWithResultViewModelFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/IHealthCheckGroupWithResultViewModelFactory.cs new file mode 100644 index 0000000000..3ee0146b29 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Factories/IHealthCheckGroupWithResultViewModelFactory.cs @@ -0,0 +1,13 @@ +using Umbraco.Cms.Api.Management.ViewModels.HealthCheck; +using Umbraco.Cms.Core.HealthChecks; + +namespace Umbraco.Cms.Api.Management.Factories; + +public interface IHealthCheckGroupWithResultViewModelFactory +{ + IEnumerable> CreateGroupingFromHealthCheckCollection(HealthCheckCollection healthChecks); + + HealthCheckGroupWithResultViewModel CreateHealthCheckGroupWithResultViewModel(IGrouping healthCheckGroup); + + HealthCheckWithResultViewModel CreateHealthCheckWithResultViewModel(HealthCheck healthCheck); +} diff --git a/src/Umbraco.Cms.Api.Management/Mapping/HealthCheck/HealthCheckViewModelsMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/HealthCheck/HealthCheckViewModelsMapDefinition.cs new file mode 100644 index 0000000000..bc70fd5f7b --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Mapping/HealthCheck/HealthCheckViewModelsMapDefinition.cs @@ -0,0 +1,80 @@ +using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.ViewModels.HealthCheck; +using Umbraco.Cms.Core.HealthChecks; +using Umbraco.Cms.Core.Mapping; + +namespace Umbraco.Cms.Api.Management.Mapping.HealthCheck; + +public class HealthCheckViewModelsMapDefinition : IMapDefinition +{ + public void DefineMaps(IUmbracoMapper mapper) + { + mapper.Define((source, context) => new HealthCheckAction(), Map); + mapper.Define((source, context) => new HealthCheckActionViewModel() { ValueRequired = false }, Map); + mapper.Define((source, context) => new HealthCheckResultViewModel() { Message = string.Empty }, Map); + mapper.Define((source, context) => new HealthCheckViewModel() { Name = string.Empty }, Map); + mapper.Define, HealthCheckGroupViewModel>((source, context) => new HealthCheckGroupViewModel() { Checks = new List() }, Map); + mapper.Define>, PagedViewModel>((source, context) => new PagedViewModel(), Map); + } + + // Umbraco.Code.MapAll -ActionParameters + private static void Map(HealthCheckActionViewModel source, HealthCheckAction target, MapperContext context) + { + target.Alias = source.Alias; + target.HealthCheckId = source.HealthCheckKey; + target.Name = source.Name; + target.Description = source.Description; + target.ValueRequired = source.ValueRequired; + target.ProvidedValueValidation = source.ProvidedValueValidation; + target.ProvidedValueValidationRegex = source.ProvidedValueValidationRegex; + target.ProvidedValue = source.ProvidedValue; + } + + // Umbraco.Code.MapAll + private static void Map(HealthCheckAction source, HealthCheckActionViewModel target, MapperContext context) + { + if (source.HealthCheckId is not null) + { + target.HealthCheckKey = (Guid)source.HealthCheckId; + } + + target.Alias = source.Alias; + target.Name = source.Name; + target.Description = source.Description; + target.ValueRequired = source.ValueRequired; + target.ProvidedValue = source.ProvidedValue; + target.ProvidedValueValidation = source.ProvidedValueValidation; + target.ProvidedValueValidationRegex = source.ProvidedValueValidationRegex; + } + + // Umbraco.Code.MapAll + private static void Map(HealthCheckStatus source, HealthCheckResultViewModel target, MapperContext context) + { + target.Message = source.Message; + target.ResultType = source.ResultType; + target.ReadMoreLink = source.ReadMoreLink; + target.Actions = context.MapEnumerable(source.Actions); + } + + // Umbraco.Code.MapAll + private static void Map(Core.HealthChecks.HealthCheck source, HealthCheckViewModel target, MapperContext context) + { + target.Key = source.Id; + target.Name = source.Name; + target.Description = source.Description; + } + + // Umbraco.Code.MapAll + private static void Map(IGrouping source, HealthCheckGroupViewModel target, MapperContext context) + { + target.Name = source.Key; + target.Checks = context.MapEnumerable(source.OrderBy(x => x.Name)); + } + + // Umbraco.Code.MapAll + private static void Map(IEnumerable> source, PagedViewModel target, MapperContext context) + { + target.Items = context.MapEnumerable, HealthCheckGroupViewModel>(source); + target.Total = source.Count(); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Languages/LanguageViewModelsMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/Languages/LanguageViewModelsMapDefinition.cs index 7849bc4d59..289f8636c9 100644 --- a/src/Umbraco.Cms.Api.Management/Mapping/Languages/LanguageViewModelsMapDefinition.cs +++ b/src/Umbraco.Cms.Api.Management/Mapping/Languages/LanguageViewModelsMapDefinition.cs @@ -1,4 +1,4 @@ -using NPoco.FluentMappings; +using NPoco.FluentMappings; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Api.Management.ViewModels.Language; @@ -50,16 +50,6 @@ public class LanguageViewModelsMapDefinition : IMapDefinition private static void Map(PagedModel source, PagedViewModel target, MapperContext context) { - if (target is null) - { - throw new ArgumentNullException(nameof(target)); - } - - if (target is not PagedViewModel list) - { - throw new NotSupportedException($"{nameof(target)} must be a List."); - } - List temp = context.MapEnumerable(source.Items); // Put the default language first in the list & then sort rest by a-z @@ -74,7 +64,7 @@ public class LanguageViewModelsMapDefinition : IMapDefinition languages.AddRange(temp.Where(x => x != defaultLang).OrderBy(x => x.Name)); } - list.Items = languages; - list.Total = source.Total; + target.Items = languages; + target.Total = source.Total; } } diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index b59dce47d7..9f9cc9a95e 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -489,9 +489,7 @@ ], "operationId": "PostDictionaryUpload", "requestBody": { - "content": { - - } + "content": {} }, "responses": { "200": { @@ -892,16 +890,6 @@ } ], "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PagedRecycleBinItem" - } - } - } - }, "401": { "description": "Unauthorized", "content": { @@ -911,6 +899,16 @@ } } } + }, + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedRecycleBinItem" + } + } + } } } } @@ -942,16 +940,6 @@ } ], "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PagedRecycleBinItem" - } - } - } - }, "401": { "description": "Unauthorized", "content": { @@ -961,6 +949,16 @@ } } } + }, + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedRecycleBinItem" + } + } + } } } } @@ -1134,6 +1132,125 @@ } } }, + "/umbraco/management/api/v1/health-check-group": { + "get": { + "tags": [ + "Health Check" + ], + "operationId": "GetHealthCheckGroup", + "parameters": [ + { + "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": { + "$ref": "#/components/schemas/PagedHealthCheckGroup" + } + } + } + } + } + } + }, + "/umbraco/management/api/v1/health-check-group/{name}": { + "get": { + "tags": [ + "Health Check" + ], + "operationId": "GetHealthCheckGroupByName", + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundResult" + } + } + } + }, + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HealthCheckGroupWithResult" + } + } + } + } + } + } + }, + "/umbraco/management/api/v1/health-check/execute-action": { + "post": { + "tags": [ + "Health Check" + ], + "operationId": "PostHealthCheckExecuteAction", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HealthCheckAction" + } + } + } + }, + "responses": { + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HealthCheckResult" + } + } + } + } + } + } + }, "/umbraco/management/api/v1/help": { "get": { "tags": [ @@ -1181,16 +1298,6 @@ } ], "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PagedHelpPage" - } - } - } - }, "400": { "description": "Bad Request", "content": { @@ -1200,6 +1307,16 @@ } } } + }, + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedHelpPage" + } + } + } } } } @@ -1259,16 +1376,6 @@ } ], "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Index" - } - } - } - }, "400": { "description": "Bad Request", "content": { @@ -1278,6 +1385,16 @@ } } } + }, + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Index" + } + } + } } } } @@ -1299,16 +1416,6 @@ } ], "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/OkResult" - } - } - } - }, "400": { "description": "Bad Request", "content": { @@ -1318,6 +1425,16 @@ } } } + }, + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OkResult" + } + } + } } } } @@ -1329,16 +1446,6 @@ ], "operationId": "GetInstallSettings", "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InstallSettings" - } - } - } - }, "400": { "description": "Bad Request", "content": { @@ -1358,6 +1465,16 @@ } } } + }, + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstallSettings" + } + } + } } } } @@ -1378,9 +1495,6 @@ } }, "responses": { - "200": { - "description": "Success" - }, "400": { "description": "Bad Request", "content": { @@ -1400,6 +1514,9 @@ } } } + }, + "200": { + "description": "Success" } } } @@ -1420,9 +1537,6 @@ } }, "responses": { - "200": { - "description": "Success" - }, "400": { "description": "Bad Request", "content": { @@ -1432,6 +1546,9 @@ } } } + }, + "200": { + "description": "Success" } } } @@ -1452,9 +1569,6 @@ } }, "responses": { - "201": { - "description": "Created" - }, "400": { "description": "Bad Request", "content": { @@ -1464,6 +1578,9 @@ } } } + }, + "201": { + "description": "Created" } } }, @@ -1522,16 +1639,6 @@ } ], "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Language" - } - } - } - }, "404": { "description": "Not Found", "content": { @@ -1541,6 +1648,16 @@ } } } + }, + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Language" + } + } + } } } }, @@ -1561,9 +1678,6 @@ } ], "responses": { - "200": { - "description": "Success" - }, "400": { "description": "Bad Request", "content": { @@ -1583,6 +1697,9 @@ } } } + }, + "200": { + "description": "Success" } } }, @@ -1612,8 +1729,15 @@ } }, "responses": { - "200": { - "description": "Success" + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundResult" + } + } + } }, "400": { "description": "Bad Request", @@ -1625,15 +1749,8 @@ } } }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundResult" - } - } - } + "200": { + "description": "Success" } } } @@ -1813,16 +1930,6 @@ } ], "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PagedRecycleBinItem" - } - } - } - }, "401": { "description": "Unauthorized", "content": { @@ -1832,6 +1939,16 @@ } } } + }, + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedRecycleBinItem" + } + } + } } } } @@ -1863,16 +1980,6 @@ } ], "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PagedRecycleBinItem" - } - } - } - }, "401": { "description": "Unauthorized", "content": { @@ -1882,6 +1989,16 @@ } } } + }, + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedRecycleBinItem" + } + } + } } } } @@ -2489,6 +2606,16 @@ } ], "responses": { + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, "200": { "description": "Success", "content": { @@ -3037,16 +3164,6 @@ ], "operationId": "GetServerStatus", "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ServerStatus" - } - } - } - }, "400": { "description": "Bad Request", "content": { @@ -3056,6 +3173,16 @@ } } } + }, + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServerStatus" + } + } + } } } } @@ -3067,16 +3194,6 @@ ], "operationId": "GetServerVersion", "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Version" - } - } - } - }, "400": { "description": "Bad Request", "content": { @@ -3086,6 +3203,16 @@ } } } + }, + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Version" + } + } + } } } } @@ -3406,9 +3533,6 @@ } }, "responses": { - "200": { - "description": "Success" - }, "400": { "description": "Bad Request", "content": { @@ -3418,6 +3542,9 @@ } } } + }, + "200": { + "description": "Success" } } } @@ -4984,6 +5111,144 @@ "type": "integer", "format": "int32" }, + "HealthCheck": { + "type": "object", + "properties": { + "key": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string", + "nullable": true + }, + "description": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "HealthCheckAction": { + "type": "object", + "properties": { + "healthCheckKey": { + "type": "string", + "format": "uuid" + }, + "alias": { + "type": "string", + "nullable": true + }, + "name": { + "type": "string", + "nullable": true + }, + "description": { + "type": "string", + "nullable": true + }, + "valueRequired": { + "type": "boolean" + }, + "providedValue": { + "type": "string", + "nullable": true + }, + "providedValueValidation": { + "type": "string", + "nullable": true + }, + "providedValueValidationRegex": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "HealthCheckGroup": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true + }, + "checks": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HealthCheck" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "HealthCheckGroupWithResult": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true + }, + "checks": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HealthCheckWithResult" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "HealthCheckResult": { + "type": "object", + "properties": { + "message": { + "type": "string", + "nullable": true + }, + "resultType": { + "$ref": "#/components/schemas/StatusResultType" + }, + "actions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HealthCheckAction" + }, + "nullable": true + }, + "readMoreLink": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "HealthCheckWithResult": { + "type": "object", + "properties": { + "key": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string", + "nullable": true + }, + "description": { + "type": "string", + "nullable": true + }, + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HealthCheckResult" + }, + "nullable": true + } + }, + "additionalProperties": false + }, "HelpPage": { "type": "object", "properties": { @@ -5053,9 +5318,7 @@ }, "providerProperties": { "type": "object", - "additionalProperties": { - - }, + "additionalProperties": {}, "nullable": true } }, @@ -5869,6 +6132,26 @@ }, "additionalProperties": false }, + "PagedHealthCheckGroup": { + "required": [ + "items", + "total" + ], + "type": "object", + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HealthCheckGroup" + } + } + }, + "additionalProperties": false + }, "PagedHelpPage": { "required": [ "items", @@ -6181,9 +6464,7 @@ "nullable": true } }, - "additionalProperties": { - - } + "additionalProperties": {} }, "ProfilingStatus": { "type": "object", @@ -6518,6 +6799,16 @@ }, "additionalProperties": false }, + "StatusResultType": { + "enum": [ + "Success", + "Warning", + "Error", + "Info" + ], + "type": "integer", + "format": "int32" + }, "StructLayoutAttribute": { "type": "object", "properties": { @@ -7339,9 +7630,7 @@ "authorizationCode": { "authorizationUrl": "/umbraco/management/api/v1.0/security/back-office/authorize", "tokenUrl": "/umbraco/management/api/v1.0/security/back-office/token", - "scopes": { - - } + "scopes": {} } } } @@ -7349,9 +7638,7 @@ }, "security": [ { - "OAuth": [ - - ] + "OAuth": [] } ] } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/HealthCheck/HealthCheckActionViewModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/HealthCheck/HealthCheckActionViewModel.cs new file mode 100644 index 0000000000..2b47fd999e --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/HealthCheck/HealthCheckActionViewModel.cs @@ -0,0 +1,50 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.HealthCheck; + +public class HealthCheckActionViewModel +{ + /// + /// Gets or sets the health check key. + /// + public Guid HealthCheckKey { get; set; } + + /// + /// Gets or sets the alias. + /// + /// + /// It is used by the Health Check instance to execute the action. + /// + public string? Alias { get; set; } + + /// + /// Gets or sets the name. + /// + /// + /// It is used to name the "Fix" button. + /// + public string? Name { get; set; } + + /// + /// Gets or sets the description. + /// + public string? Description { get; set; } + + /// + /// Gets or sets a value indicating whether a value is required to rectify the issue. + /// + public required bool ValueRequired { get; set; } + + /// + /// Gets or sets the value to rectify the issue. + /// + public string? ProvidedValue { get; set; } + + /// + /// Gets or sets how the provided value is validated. + /// + public string? ProvidedValueValidation { get; set; } + + /// + /// Gets or sets the regex to use when validating the provided value (if the value can be validated by a regex). + /// + public string? ProvidedValueValidationRegex { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/HealthCheck/HealthCheckGroupViewModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/HealthCheck/HealthCheckGroupViewModel.cs new file mode 100644 index 0000000000..74b5995d37 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/HealthCheck/HealthCheckGroupViewModel.cs @@ -0,0 +1,14 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.HealthCheck; + +public class HealthCheckGroupViewModel +{ + /// + /// Gets or sets the name. + /// + public string? Name { get; set; } + + /// + /// Gets or sets the health checks. + /// + public required List Checks { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/HealthCheck/HealthCheckGroupWithResultViewModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/HealthCheck/HealthCheckGroupWithResultViewModel.cs new file mode 100644 index 0000000000..a5ad99eea0 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/HealthCheck/HealthCheckGroupWithResultViewModel.cs @@ -0,0 +1,14 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.HealthCheck; + +public class HealthCheckGroupWithResultViewModel +{ + /// + /// Gets or sets the name. + /// + public string? Name { get; set; } + + /// + /// Gets or sets the health checks with the result(s) from each health check. + /// + public required List Checks { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/HealthCheck/HealthCheckResultViewModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/HealthCheck/HealthCheckResultViewModel.cs new file mode 100644 index 0000000000..f4c89b7b34 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/HealthCheck/HealthCheckResultViewModel.cs @@ -0,0 +1,26 @@ +using Umbraco.Cms.Core.HealthChecks; + +namespace Umbraco.Cms.Api.Management.ViewModels.HealthCheck; + +public class HealthCheckResultViewModel +{ + /// + /// Gets or sets the status message. + /// + public required string Message { get; set; } + + /// + /// Gets or sets the status type. + /// + public StatusResultType ResultType { get; set; } + + /// + /// Gets or sets the potential actions to take (if any). + /// + public IEnumerable? Actions { get; set; } + + /// + /// This is optional but would allow a developer to get or set a link that is shown as a "Read more" button. + /// + public string? ReadMoreLink { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/HealthCheck/HealthCheckViewModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/HealthCheck/HealthCheckViewModel.cs new file mode 100644 index 0000000000..39cde4843e --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/HealthCheck/HealthCheckViewModel.cs @@ -0,0 +1,19 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.HealthCheck; + +public class HealthCheckViewModel +{ + /// + /// Gets or sets the key. + /// + public Guid Key { get; set; } + + /// + /// Gets or sets the name. + /// + public required string Name { get; set; } + + /// + /// Gets or sets the description. + /// + public string? Description { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/HealthCheck/HealthCheckWithResultViewModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/HealthCheck/HealthCheckWithResultViewModel.cs new file mode 100644 index 0000000000..c566ae86b8 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/HealthCheck/HealthCheckWithResultViewModel.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.HealthCheck; + +public class HealthCheckWithResultViewModel : HealthCheckViewModel +{ + /// + /// Gets or sets the result(s) for a health check. + /// There can be several. + /// + public List? Results { get; set; } +} diff --git a/src/Umbraco.Core/Constants-HealthChecks.cs b/src/Umbraco.Core/Constants-HealthChecks.cs index 2980a59457..94633c4502 100644 --- a/src/Umbraco.Core/Constants-HealthChecks.cs +++ b/src/Umbraco.Core/Constants-HealthChecks.cs @@ -10,6 +10,11 @@ public static partial class Constants /// public static class HealthChecks { + public static class RoutePath + { + public const string HealthCheck = "health-check"; + } + public static class DocumentationLinks { public const string SmtpCheck = "https://umbra.co/healthchecks-smtp";