diff --git a/src/Squidex/Areas/Api/Config/Swagger/FixProcessor.cs b/src/Squidex/Areas/Api/Config/Swagger/FixProcessor.cs index 83106901f..0131711f9 100644 --- a/src/Squidex/Areas/Api/Config/Swagger/FixProcessor.cs +++ b/src/Squidex/Areas/Api/Config/Swagger/FixProcessor.cs @@ -13,7 +13,7 @@ using Squidex.Infrastructure.Tasks; namespace Squidex.Areas.Api.Config.Swagger { - public class FixProcessor : IOperationProcessor + public sealed class FixProcessor : IOperationProcessor { private static readonly JsonSchema4 StringSchema = new JsonSchema4 { Type = JsonObjectType.String }; diff --git a/src/Squidex/Areas/Api/Config/Swagger/SecurityProcessor.cs b/src/Squidex/Areas/Api/Config/Swagger/SecurityProcessor.cs index a09e9c9f0..8dfa61f6e 100644 --- a/src/Squidex/Areas/Api/Config/Swagger/SecurityProcessor.cs +++ b/src/Squidex/Areas/Api/Config/Swagger/SecurityProcessor.cs @@ -15,7 +15,7 @@ using Squidex.Web; namespace Squidex.Areas.Api.Config.Swagger { - public class SecurityProcessor : SecurityDefinitionAppender + public sealed class SecurityProcessor : SecurityDefinitionAppender { public SecurityProcessor(IOptions urlOptions) : base(Constants.SecurityDefinition, Enumerable.Empty(), CreateOAuthSchema(urlOptions.Value)) @@ -24,26 +24,33 @@ namespace Squidex.Areas.Api.Config.Swagger private static SwaggerSecurityScheme CreateOAuthSchema(UrlsOptions urlOptions) { - var securityScheme = new SwaggerSecurityScheme(); + var security = new SwaggerSecurityScheme + { + Type = SwaggerSecuritySchemeType.OAuth2 + }; var tokenUrl = urlOptions.BuildUrl($"{Constants.IdentityServerPrefix}/connect/token", false); - securityScheme.TokenUrl = tokenUrl; + security.TokenUrl = tokenUrl; - var securityDocs = NSwagHelper.LoadDocs("security"); - var securityText = securityDocs.Replace("", tokenUrl); + SetupDescription(security, tokenUrl); - securityScheme.Description = securityText; - - securityScheme.Type = SwaggerSecuritySchemeType.OAuth2; - securityScheme.Flow = SwaggerOAuth2Flow.Application; + security.Flow = SwaggerOAuth2Flow.Application; - securityScheme.Scopes = new Dictionary + security.Scopes = new Dictionary { [Constants.ApiScope] = "Read and write access to the API" }; - return securityScheme; + return security; + } + + private static void SetupDescription(SwaggerSecurityScheme securityScheme, string tokenUrl) + { + var securityDocs = NSwagHelper.LoadDocs("security"); + var securityText = securityDocs.Replace("", tokenUrl); + + securityScheme.Description = securityText; } } } diff --git a/src/Squidex/Areas/Api/Config/Swagger/SwaggerServices.cs b/src/Squidex/Areas/Api/Config/Swagger/SwaggerServices.cs index ddccd616c..f3b32121f 100644 --- a/src/Squidex/Areas/Api/Config/Swagger/SwaggerServices.cs +++ b/src/Squidex/Areas/Api/Config/Swagger/SwaggerServices.cs @@ -22,6 +22,9 @@ namespace Squidex.Areas.Api.Config.Swagger { public static void AddMySwaggerSettings(this IServiceCollection services) { + services.AddSingletonAs() + .As(); + services.AddSingletonAs() .As(); diff --git a/src/Squidex/Areas/Api/Config/Swagger/XmlResponseTypesProcessor.cs b/src/Squidex/Areas/Api/Config/Swagger/XmlResponseTypesProcessor.cs index dbcb2601f..fedc54045 100644 --- a/src/Squidex/Areas/Api/Config/Swagger/XmlResponseTypesProcessor.cs +++ b/src/Squidex/Areas/Api/Config/Swagger/XmlResponseTypesProcessor.cs @@ -6,16 +6,12 @@ // ========================================================================== using System; -using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; using NJsonSchema.Infrastructure; using NSwag; using NSwag.SwaggerGeneration.Processors; using NSwag.SwaggerGeneration.Processors.Contexts; -using Squidex.Pipeline.Swagger; - -#pragma warning disable RECS0033 // Convert 'if' to '||' expression namespace Squidex.Areas.Api.Config.Swagger { @@ -53,42 +49,7 @@ namespace Squidex.Areas.Api.Config.Swagger } } - await AddInternalErrorResponseAsync(context, operation); - - CleanupResponses(operation); - return true; } - - private static async Task AddInternalErrorResponseAsync(OperationProcessorContext context, SwaggerOperation operation) - { - var errorSchema = await context.SchemaGenerator.GetErrorDtoSchemaAsync(context.SchemaResolver); - - if (!operation.Responses.ContainsKey("500")) - { - operation.AddResponse("500", "Operation failed", errorSchema); - } - - foreach (var (code, response) in operation.Responses) - { - if (code != "404" && code.StartsWith("4", StringComparison.OrdinalIgnoreCase) && response.Schema == null) - { - response.Schema = errorSchema; - } - } - } - - private static void CleanupResponses(SwaggerOperation operation) - { - foreach (var (code, response) in operation.Responses.ToList()) - { - if (string.IsNullOrWhiteSpace(response.Description) || - response.Description?.Contains("=>") == true || - response.Description?.Contains("=>") == true) - { - operation.Responses.Remove(code); - } - } - } } } \ No newline at end of file diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaSwaggerGenerator.cs b/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaSwaggerGenerator.cs index 4b7cd1997..4aad54547 100644 --- a/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaSwaggerGenerator.cs +++ b/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaSwaggerGenerator.cs @@ -32,6 +32,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator private readonly string schemaName; private readonly string schemaType; private readonly string appPath; + private readonly JsonSchema4 statusSchema; private readonly string appName; static SchemaSwaggerGenerator() @@ -46,6 +47,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator string appPath, Schema schema, SchemaResolver schemaResolver, + JsonSchema4 statusSchema, PartitionResolver partitionResolver) { this.document = document; @@ -53,6 +55,8 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator this.appName = appName; this.appPath = appPath; + this.statusSchema = statusSchema; + schemaPath = schema.Name; schemaName = schema.DisplayName(); schemaType = schema.TypeName(); @@ -72,15 +76,13 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator var schemaOperations = new List { - GenerateSchemaQueryOperation(), - GenerateSchemaCreateOperation(), + GenerateSchemaGetsOperation(), GenerateSchemaGetOperation(), + GenerateSchemaCreateOperation(), GenerateSchemaUpdateOperation(), - GenerateSchemaPatchOperation(), - GenerateSchemaPublishOperation(), - GenerateSchemaUnpublishOperation(), - GenerateSchemaArchiveOperation(), - GenerateSchemaRestoreOperation(), + GenerateSchemaUpdatePatchOperation(), + GenerateSchemaStatusOperation(), + GenerateSchemaDiscardOperation(), GenerateSchemaDeleteOperation() }; @@ -90,11 +92,12 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator } } - private SwaggerPathItem GenerateSchemaQueryOperation() + private SwaggerPathItem GenerateSchemaGetsOperation() { return AddOperation(SwaggerOperationMethod.Get, null, $"{appPath}/{schemaPath}", operation => { operation.OperationId = $"Query{schemaType}Contents"; + operation.Summary = $"Queries {schemaName} contents."; operation.Description = SchemaQueryDescription; @@ -103,7 +106,8 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator operation.AddQueryParameter("$skip", JsonObjectType.Number, "Optional number of contents to skip."); operation.AddQueryParameter("$filter", JsonObjectType.String, "Optional OData filter."); operation.AddQueryParameter("$search", JsonObjectType.String, "Optional OData full text search."); - operation.AddQueryParameter("orderby", JsonObjectType.String, "Optional OData order definition."); + operation.AddQueryParameter("$orderby", JsonObjectType.String, "Optional OData order definition."); + operation.AddQueryParameter("$orderby", JsonObjectType.String, "Optional OData order definition."); operation.AddResponse("200", $"{schemaName} content retrieved.", CreateContentsSchema(schemaName, contentSchema)); @@ -116,6 +120,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator return AddOperation(SwaggerOperationMethod.Get, schemaName, $"{appPath}/{schemaPath}/{{id}}", operation => { operation.OperationId = $"Get{schemaType}Content"; + operation.Summary = $"Get a {schemaName} content."; operation.AddResponse("200", $"{schemaName} content found.", contentSchema); @@ -129,12 +134,14 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator return AddOperation(SwaggerOperationMethod.Post, null, $"{appPath}/{schemaPath}", operation => { operation.OperationId = $"Create{schemaType}Content"; + operation.Summary = $"Create a {schemaName} content."; operation.AddBodyParameter("data", dataSchema, SchemaBodyDescription); operation.AddQueryParameter("publish", JsonObjectType.Boolean, "Set to true to autopublish content."); - operation.AddResponse("201", $"{schemaName} content created.", contentSchema); + operation.AddResponse("200", $"{schemaName} content created.", contentSchema); + operation.AddResponse("400", "Content data valid."); AddSecurity(operation, Permissions.AppContentsCreate); }); @@ -145,74 +152,64 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator return AddOperation(SwaggerOperationMethod.Put, schemaName, $"{appPath}/{schemaPath}/{{id}}", operation => { operation.OperationId = $"Update{schemaType}Content"; + operation.Summary = $"Update a {schemaName} content."; operation.AddBodyParameter("data", dataSchema, SchemaBodyDescription); - operation.AddResponse("200", $"{schemaName} content updated.", dataSchema); + operation.AddResponse("200", $"{schemaName} content updated.", contentSchema); + operation.AddResponse("400", "Content data valid."); AddSecurity(operation, Permissions.AppContentsUpdate); }); } - private SwaggerPathItem GenerateSchemaPatchOperation() + private SwaggerPathItem GenerateSchemaUpdatePatchOperation() { return AddOperation(SwaggerOperationMethod.Patch, schemaName, $"{appPath}/{schemaPath}/{{id}}", operation => { operation.OperationId = $"Path{schemaType}Content"; + operation.Summary = $"Patch a {schemaName} content."; operation.AddBodyParameter("data", dataSchema, SchemaBodyDescription); - operation.AddResponse("200", $"{schemaName} content patched.", dataSchema); + operation.AddResponse("200", $"{schemaName} content patched.", contentSchema); + operation.AddResponse("400", "Status change not valid."); AddSecurity(operation, Permissions.AppContentsUpdate); }); } - private SwaggerPathItem GenerateSchemaPublishOperation() + private SwaggerPathItem GenerateSchemaStatusOperation() { - return AddOperation(SwaggerOperationMethod.Put, schemaName, $"{appPath}/{schemaPath}/{{id}}/publish", operation => + return AddOperation(SwaggerOperationMethod.Put, schemaName, $"{appPath}/{schemaPath}/{{id}}/status", operation => { - operation.OperationId = $"Publish{schemaType}Content"; - operation.Summary = $"Publish a {schemaName} content."; + operation.OperationId = $"Change{schemaType}ContentStatus"; - operation.AddResponse("204", $"{schemaName} content published."); - }); - } + operation.Summary = $"Change status of {schemaName} content."; - private SwaggerPathItem GenerateSchemaUnpublishOperation() - { - return AddOperation(SwaggerOperationMethod.Put, schemaName, $"{appPath}/{schemaPath}/{{id}}/unpublish", operation => - { - operation.OperationId = $"Unpublish{schemaType}Content"; - operation.Summary = $"Unpublish a {schemaName} content."; + operation.AddBodyParameter("request", statusSchema, "The request to change content status."); + + operation.AddResponse("204", $"{schemaName} content status changed.", contentSchema); + operation.AddResponse("400", "Content data valid."); - operation.AddResponse("204", $"{schemaName} content unpublished."); + AddSecurity(operation, Permissions.AppContentsStatus); }); } - private SwaggerPathItem GenerateSchemaArchiveOperation() + private SwaggerPathItem GenerateSchemaDiscardOperation() { - return AddOperation(SwaggerOperationMethod.Put, schemaName, $"{appPath}/{schemaPath}/{{id}}/archive", operation => + return AddOperation(SwaggerOperationMethod.Put, schemaName, $"{appPath}/{schemaPath}/{{id}}/discard", operation => { - operation.OperationId = $"Archive{schemaType}Content"; - operation.Summary = $"Archive a {schemaName} content."; + operation.OperationId = $"Discard{schemaType}Content"; - operation.AddResponse("204", $"{schemaName} content restored."); - - AddSecurity(operation, Permissions.AppContentsRead); - }); - } + operation.Summary = $"Discard changes of {schemaName} content."; - private SwaggerPathItem GenerateSchemaRestoreOperation() - { - return AddOperation(SwaggerOperationMethod.Put, schemaName, $"{appPath}/{schemaPath}/{{id}}/restore", operation => - { - operation.OperationId = $"Restore{schemaType}Content"; - operation.Summary = $"Restore a {schemaName} content."; + operation.AddResponse("400", "No pending draft."); + operation.AddResponse("200", $"{schemaName} content status changed.", contentSchema); - operation.AddResponse("204", $"{schemaName} content restored."); + AddSecurity(operation, Permissions.AppContentsDiscard); }); } @@ -221,6 +218,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator return AddOperation(SwaggerOperationMethod.Delete, schemaName, $"{appPath}/{schemaPath}/{{id}}/", operation => { operation.OperationId = $"Delete{schemaType}Content"; + operation.Summary = $"Delete a {schemaName} content."; operation.AddResponse("204", $"{schemaName} content deleted."); diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasSwaggerGenerator.cs b/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasSwaggerGenerator.cs index e2e7a8d2c..7ea592429 100644 --- a/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasSwaggerGenerator.cs +++ b/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasSwaggerGenerator.cs @@ -18,6 +18,7 @@ using NSwag.SwaggerGeneration; using NSwag.SwaggerGeneration.Processors; using NSwag.SwaggerGeneration.Processors.Contexts; using Squidex.Areas.Api.Config.Swagger; +using Squidex.Areas.Api.Controllers.Contents.Models; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; @@ -32,6 +33,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator private readonly SwaggerDocumentSettings settings = new SwaggerDocumentSettings(); private SwaggerJsonSchemaGenerator schemaGenerator; private SwaggerDocument document; + private JsonSchema4 statusSchema; private JsonSchemaResolver schemaResolver; public SchemasSwaggerGenerator(IOptions urlOptions, IEnumerable documentProcessors) @@ -53,9 +55,9 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator schemaGenerator = new SwaggerJsonSchemaGenerator(settings); schemaResolver = new SwaggerSchemaResolver(document, settings); - GenerateSchemasOperations(schemas, app); + statusSchema = await GenerateStatusSchemaAsync(); - await GenerateDefaultErrorsAsync(); + GenerateSchemasOperations(schemas, app); var context = new DocumentProcessorContext(document, @@ -73,25 +75,23 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator return document; } - private void GenerateSchemasOperations(IEnumerable schemas, IAppEntity app) + private Task GenerateStatusSchemaAsync() { - var appBasePath = $"/content/{app.Name}"; + var errorType = typeof(ChangeStatusDto); - foreach (var schema in schemas.Select(x => x.SchemaDef).Where(x => x.IsPublished)) - { - new SchemaSwaggerGenerator(document, app.Name, appBasePath, schema, AppendSchema, app.PartitionResolver()).GenerateSchemaOperations(); - } + return schemaGenerator.GenerateWithReferenceAsync(errorType, Enumerable.Empty(), schemaResolver); } - private async Task GenerateDefaultErrorsAsync() + private void GenerateSchemasOperations(IEnumerable schemas, IAppEntity app) { - const string errorDescription = "Operation failed with internal server error."; - - var errorDtoSchema = await schemaGenerator.GetErrorDtoSchemaAsync(schemaResolver); + var appBasePath = $"/content/{app.Name}"; - foreach (var operation in document.Paths.Values.SelectMany(x => x.Values)) + foreach (var schema in schemas.Select(x => x.SchemaDef).Where(x => x.IsPublished)) { - operation.Responses.Add("500", new SwaggerResponse { Description = errorDescription, Schema = errorDtoSchema }); + var partition = app.PartitionResolver(); + + new SchemaSwaggerGenerator(document, app.Name, appBasePath, schema, AppendSchema, statusSchema, partition) + .GenerateSchemaOperations(); } } diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs index 96a1cb900..3939cfd67 100644 --- a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs @@ -130,14 +130,14 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models if (controller.HasPermission(Permissions.AppContentsUpdate, app, schema)) { - AddPatchLink("patch", controller.Url(x => nameof(x.PatchContent), values)); - AddPutLink("update", controller.Url(x => nameof(x.PutContent), values)); if (Status == Status.Published) { AddPutLink("draft/propose", controller.Url(x => nameof(x.PutContent), values) + "?asDraft=true"); } + + AddPatchLink("patch", controller.Url(x => nameof(x.PatchContent), values)); } if (controller.HasPermission(Permissions.AppContentsDelete, app, schema)) diff --git a/src/Squidex/Pipeline/Swagger/NSwagHelper.cs b/src/Squidex/Pipeline/Swagger/NSwagHelper.cs index 385a2b48f..edeb39d1f 100644 --- a/src/Squidex/Pipeline/Swagger/NSwagHelper.cs +++ b/src/Squidex/Pipeline/Swagger/NSwagHelper.cs @@ -8,11 +8,8 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using NJsonSchema; -using NJsonSchema.Generation; using NSwag; using Squidex.Web; @@ -69,13 +66,6 @@ namespace Squidex.Pipeline.Swagger return document; } - public static async Task GetErrorDtoSchemaAsync(this JsonSchemaGenerator schemaGenerator, JsonSchemaResolver resolver) - { - var errorType = typeof(ErrorDto); - - return await schemaGenerator.GenerateWithReferenceAsync(errorType, Enumerable.Empty(), resolver); - } - public static void AddQueryParameter(this SwaggerOperation operation, string name, JsonObjectType type, string description = null) { var parameter = new SwaggerParameter { Type = type, Name = name, Kind = SwaggerParameterKind.Query };