Browse Source

Swagger improvements.

pull/336/head
Sebastian Stehle 7 years ago
parent
commit
bfe2e37686
  1. 29
      src/Squidex/Areas/Api/Config/Swagger/ScopesProcessor.cs
  2. 4
      src/Squidex/Areas/Api/Config/Swagger/SecurityProcessor.cs
  3. 6
      src/Squidex/Areas/Api/Config/Swagger/SwaggerExtensions.cs
  4. 3
      src/Squidex/Areas/Api/Config/Swagger/SwaggerServices.cs
  5. 7
      src/Squidex/Areas/Api/Config/Swagger/XmlTagProcessor.cs
  6. 14
      src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs
  7. 1
      src/Squidex/Areas/Api/Controllers/Contents/ContentSwaggerController.cs
  8. 2
      src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs
  9. 40
      src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaSwaggerGenerator.cs
  10. 4
      src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasSwaggerGenerator.cs
  11. 1
      src/Squidex/Areas/Api/Controllers/Docs/DocsController.cs
  12. 1
      src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs
  13. 52
      src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionProcessor.cs
  14. 4
      src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs
  15. 1
      src/Squidex/Areas/Api/Controllers/Rules/TwitterController.cs
  16. 2
      src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs
  17. 1
      src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs
  18. 1
      src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs
  19. 1
      src/Squidex/Areas/IdentityServer/Controllers/IdentityServerController.cs
  20. 1
      src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs
  21. 6
      src/Squidex/Pipeline/ApiPermissionAttribute.cs
  22. 4
      src/Squidex/Pipeline/Swagger/NSwagHelper.cs

29
src/Squidex/Areas/Api/Config/Swagger/ScopesProcessor.cs

@ -15,6 +15,7 @@ using NSwag.SwaggerGeneration.Processors;
using NSwag.SwaggerGeneration.Processors.Contexts;
using Squidex.Config;
using Squidex.Infrastructure.Tasks;
using Squidex.Pipeline;
namespace Squidex.Areas.Api.Config.Swagger
{
@ -27,20 +28,32 @@ namespace Squidex.Areas.Api.Config.Swagger
context.OperationDescription.Operation.Security = new List<SwaggerSecurityRequirement>();
}
var authorizeAttributes =
context.MethodInfo.GetCustomAttributes<AuthorizeAttribute>(true).Union(
context.MethodInfo.DeclaringType.GetCustomAttributes<AuthorizeAttribute>(true))
.ToArray();
var permissionAttribute = context.MethodInfo.GetCustomAttribute<ApiPermissionAttribute>();
if (authorizeAttributes.Any())
if (permissionAttribute != null)
{
var scopes = authorizeAttributes.Where(a => a.Roles != null).SelectMany(a => a.Roles.Split(',')).Distinct().ToList();
context.OperationDescription.Operation.Security.Add(new SwaggerSecurityRequirement
{
{ Constants.SecurityDefinition, scopes }
[Constants.SecurityDefinition] = permissionAttribute.PermissionIds
});
}
else
{
var authorizeAttributes =
context.MethodInfo.GetCustomAttributes<AuthorizeAttribute>(true).Union(
context.MethodInfo.DeclaringType.GetCustomAttributes<AuthorizeAttribute>(true))
.ToArray();
if (authorizeAttributes.Any())
{
var scopes = authorizeAttributes.Where(a => a.Roles != null).SelectMany(a => a.Roles.Split(',')).Distinct().ToList();
context.OperationDescription.Operation.Security.Add(new SwaggerSecurityRequirement
{
[Constants.SecurityDefinition] = scopes
});
}
}
return TaskHelper.True;
}

4
src/Squidex/Areas/Api/Config/Swagger/SecurityProcessor.cs

@ -29,7 +29,7 @@ namespace Squidex.Areas.Api.Config.Swagger
securityScheme.TokenUrl = tokenUrl;
var securityDocs = SwaggerHelper.LoadDocs("security");
var securityDocs = NSwagHelper.LoadDocs("security");
var securityText = securityDocs.Replace("<TOKEN_URL>", tokenUrl);
securityScheme.Description = securityText;
@ -39,7 +39,7 @@ namespace Squidex.Areas.Api.Config.Swagger
securityScheme.Scopes = new Dictionary<string, string>
{
{ Constants.ApiScope, "Read and write access to the API" }
[Constants.ApiScope] = "Read and write access to the API"
};
return securityScheme;

6
src/Squidex/Areas/Api/Config/Swagger/SwaggerExtensions.cs

@ -6,7 +6,6 @@
// ==========================================================================
using Microsoft.AspNetCore.Builder;
using Squidex.Config;
namespace Squidex.Areas.Api.Config.Swagger
{
@ -14,10 +13,7 @@ namespace Squidex.Areas.Api.Config.Swagger
{
public static void UseMySwagger(this IApplicationBuilder app)
{
app.UseSwagger(settings =>
{
settings.Path = $"{Constants.ApiPrefix}/swagger/{{documentName}}/swagger.json";
});
app.UseSwagger();
}
}
}

3
src/Squidex/Areas/Api/Config/Swagger/SwaggerServices.cs

@ -32,6 +32,9 @@ namespace Squidex.Areas.Api.Config.Swagger
services.AddSingletonAs<XmlTagProcessor>()
.As<IDocumentProcessor>();
services.AddSingletonAs<SecurityProcessor>()
.As<IDocumentProcessor>();
services.AddSingletonAs<ScopesProcessor>()
.As<IOperationProcessor>();

7
src/Squidex/Areas/Api/Config/Swagger/XmlTagProcessor.cs

@ -8,6 +8,7 @@
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using NJsonSchema.Infrastructure;
using NSwag.Annotations;
using NSwag.SwaggerGeneration.Processors;
@ -22,11 +23,11 @@ namespace Squidex.Areas.Api.Config.Swagger
{
foreach (var controllerType in context.ControllerTypes)
{
var tagAttribute = controllerType.GetTypeInfo().GetCustomAttribute<SwaggerTagAttribute>();
var attribute = controllerType.GetTypeInfo().GetCustomAttribute<ApiExplorerSettingsAttribute>();
if (tagAttribute != null)
if (attribute != null)
{
var tag = context.Document.Tags.FirstOrDefault(x => x.Name == tagAttribute.Name);
var tag = context.Document.Tags.FirstOrDefault(x => x.Name == attribute.GroupName);
if (tag != null)
{

14
src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs

@ -20,6 +20,7 @@ namespace Squidex.Areas.Api.Controllers.Backups
/// <summary>
/// Restores backups.
/// </summary>
[ApiExplorerSettings(GroupName = nameof(Backups))]
[ApiModelValidation(true)]
public class RestoreController : ApiController
{
@ -31,6 +32,12 @@ namespace Squidex.Areas.Api.Controllers.Backups
this.grainFactory = grainFactory;
}
/// <summary>
/// Get current status.
/// </summary>
/// <returns>
/// 200 => Status returned.
/// </returns>
[HttpGet]
[Route("apps/restore/")]
[ApiPermission(Permissions.AdminRestoreRead)]
@ -50,6 +57,13 @@ namespace Squidex.Areas.Api.Controllers.Backups
return Ok(response);
}
/// <summary>
/// Restore a backup.
/// </summary>
/// <param name="request">The backup to restore.</param>
/// <returns>
/// 204 => Restore operation started.
/// </returns>
[HttpPost]
[Route("apps/restore/")]
[ApiPermission(Permissions.AdminRestoreCreate)]

1
src/Squidex/Areas/Api/Controllers/Contents/ContentSwaggerController.cs

@ -15,6 +15,7 @@ using Squidex.Pipeline;
namespace Squidex.Areas.Api.Controllers.Contents
{
[ApiExplorerSettings(IgnoreApi = true)]
public sealed class ContentSwaggerController : ApiController
{
private readonly IAppProvider appProvider;

2
src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs

@ -75,7 +75,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
}
/// <summary>
/// GraphQL endpoint with batch support.
/// GraphQL endpoint (Batch).
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="batch">The graphql queries.</param>

40
src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaSwaggerGenerator.cs

@ -10,12 +10,14 @@ using System.Collections.Generic;
using System.Linq;
using NJsonSchema;
using NSwag;
using Squidex.Config;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.GenerateJsonSchema;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Pipeline.Swagger;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Contents.Generator
{
@ -31,18 +33,20 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
private readonly string schemaName;
private readonly string schemaType;
private readonly string appPath;
private readonly string appName;
static SchemaSwaggerGenerator()
{
SchemaBodyDescription = SwaggerHelper.LoadDocs("schemabody");
SchemaQueryDescription = SwaggerHelper.LoadDocs("schemaquery");
SchemaBodyDescription = NSwagHelper.LoadDocs("schemabody");
SchemaQueryDescription = NSwagHelper.LoadDocs("schemaquery");
}
public SchemaSwaggerGenerator(SwaggerDocument document, string path, Schema schema, Func<string, JsonSchema4, JsonSchema4> schemaResolver, PartitionResolver partitionResolver)
public SchemaSwaggerGenerator(SwaggerDocument document, string appName, string appPath, Schema schema, Func<string, JsonSchema4, JsonSchema4> schemaResolver, PartitionResolver partitionResolver)
{
this.document = document;
appPath = path;
this.appName = appName;
this.appPath = appPath;
schemaPath = schema.Name;
schemaName = schema.DisplayName();
@ -97,6 +101,8 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
operation.AddQueryParameter("orderby", JsonObjectType.String, "Optional OData order definition.");
operation.AddResponse("200", $"{schemaName} content retrieved.", CreateContentsSchema(schemaName, contentSchema));
AddSecurity(operation, Permissions.AppContentsRead);
});
}
@ -108,6 +114,8 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
operation.Summary = $"Get a {schemaName} content.";
operation.AddResponse("200", $"{schemaName} content found.", contentSchema);
AddSecurity(operation, Permissions.AppContentsRead);
});
}
@ -122,6 +130,8 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
operation.AddQueryParameter("publish", JsonObjectType.Boolean, "Set to true to autopublish content.");
operation.AddResponse("201", $"{schemaName} content created.", contentSchema);
AddSecurity(operation, Permissions.AppContentsCreate);
});
}
@ -135,6 +145,8 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
operation.AddBodyParameter("data", dataSchema, SchemaBodyDescription);
operation.AddResponse("200", $"{schemaName} content updated.", dataSchema);
AddSecurity(operation, Permissions.AppContentsUpdate);
});
}
@ -148,6 +160,8 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
operation.AddBodyParameter("data", dataSchema, SchemaBodyDescription);
operation.AddResponse("200", $"{schemaName} content patched.", dataSchema);
AddSecurity(operation, Permissions.AppContentsUpdate);
});
}
@ -159,6 +173,8 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
operation.Summary = $"Publish a {schemaName} content.";
operation.AddResponse("204", $"{schemaName} content published.");
AddSecurity(operation, Permissions.AppContentsPublish);
});
}
@ -170,6 +186,8 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
operation.Summary = $"Unpublish a {schemaName} content.";
operation.AddResponse("204", $"{schemaName} content unpublished.");
AddSecurity(operation, Permissions.AppContentsUnpublish);
});
}
@ -181,6 +199,8 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
operation.Summary = $"Archive a {schemaName} content.";
operation.AddResponse("204", $"{schemaName} content restored.");
AddSecurity(operation, Permissions.AppContentsRead);
});
}
@ -192,6 +212,8 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
operation.Summary = $"Restore a {schemaName} content.";
operation.AddResponse("204", $"{schemaName} content restored.");
AddSecurity(operation, Permissions.AppContentsRestore);
});
}
@ -203,6 +225,8 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
operation.Summary = $"Delete a {schemaName} content.";
operation.AddResponse("204", $"{schemaName} content deleted.");
AddSecurity(operation, Permissions.AppContentsDelete);
});
}
@ -245,5 +269,13 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
return schema;
}
private void AddSecurity(SwaggerOperation operation, string permission)
{
operation.Security.Add(new SwaggerSecurityRequirement
{
[Constants.SecurityDefinition] = new[] { Permissions.ForApp(permission, appName, schemaPath).Id }
});
}
}
}

4
src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasSwaggerGenerator.cs

@ -48,7 +48,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
public async Task<SwaggerDocument> Generate(HttpContext httpContext, IAppEntity app, IEnumerable<ISchemaEntity> schemas)
{
document = SwaggerHelper.CreateApiDocument(httpContext, urlOptions, app.Name);
document = NSwagHelper.CreateApiDocument(httpContext, urlOptions, app.Name);
schemaGenerator = new SwaggerJsonSchemaGenerator(settings);
schemaResolver = new SwaggerSchemaResolver(document, settings);
@ -79,7 +79,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
foreach (var schema in schemas.Where(x => x.IsPublished).Select(x => x.SchemaDef))
{
new SchemaSwaggerGenerator(document, appBasePath, schema, AppendSchema, app.PartitionResolver()).GenerateSchemaOperations();
new SchemaSwaggerGenerator(document, app.Name, appBasePath, schema, AppendSchema, app.PartitionResolver()).GenerateSchemaOperations();
}
}

1
src/Squidex/Areas/Api/Controllers/Docs/DocsController.cs

@ -10,6 +10,7 @@ using Squidex.Infrastructure.Commands;
namespace Squidex.Areas.Api.Controllers.Docs
{
[ApiExplorerSettings(IgnoreApi = true)]
public sealed class DocsController : ApiController
{
public DocsController(ICommandBus commandBus)

1
src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs

@ -18,6 +18,7 @@ using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.EventConsumers
{
[ApiExplorerSettings(IgnoreApi = true)]
public sealed class EventConsumersController : ApiController
{
private readonly IGrainFactory grainFactory;

52
src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionProcessor.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NJsonSchema;
@ -19,35 +20,44 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models
{
public async Task ProcessAsync(DocumentProcessorContext context)
{
var schema = context.SchemaResolver.GetSchema(typeof(RuleAction), false);
if (schema != null)
try
{
var discriminator = new OpenApiDiscriminator
{
JsonInheritanceConverter = new JsonInheritanceConverter("actionType", typeof(RuleAction)), PropertyName = "actionType"
};
schema.DiscriminatorObject = discriminator;
schema.Properties["actionType"] = new JsonProperty
{
Type = JsonObjectType.String, IsRequired = true
};
var schema = context.SchemaResolver.GetSchema(typeof(RuleAction), false);
foreach (var derived in RuleElementRegistry.Actions)
if (schema != null)
{
var derivedSchema = await context.SchemaGenerator.GenerateAsync(derived.Value.Type, context.SchemaResolver);
var discriminator = new OpenApiDiscriminator
{
JsonInheritanceConverter = new JsonInheritanceConverter("actionType", typeof(RuleAction)),
PropertyName = "actionType"
};
var oldName = context.Document.Definitions.FirstOrDefault(x => x.Value == derivedSchema).Key;
schema.DiscriminatorObject = discriminator;
schema.Properties["actionType"] = new JsonProperty
{
Type = JsonObjectType.String,
IsRequired = true
};
if (oldName != null)
foreach (var derived in RuleElementRegistry.Actions)
{
context.Document.Definitions.Remove(oldName);
context.Document.Definitions.Add(derived.Key, derivedSchema);
var derivedSchema = await context.SchemaGenerator.GenerateAsync(derived.Value.Type, context.SchemaResolver);
var oldName = context.Document.Definitions.FirstOrDefault(x => x.Value == derivedSchema).Key;
if (oldName != null)
{
context.Document.Definitions.Remove(oldName);
context.Document.Definitions.Add(derived.Key, derivedSchema);
}
}
}
RemoveFreezable(context, schema);
RemoveFreezable(context, schema);
}
}
catch (KeyNotFoundException)
{
return;
}
}

4
src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs

@ -250,7 +250,7 @@ namespace Squidex.Areas.Api.Controllers.Rules
}
/// <summary>
/// Enqueue the event to retry it immediate
/// Retry the event immediately.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="id">The event to enqueue.</param>
@ -277,7 +277,7 @@ namespace Squidex.Areas.Api.Controllers.Rules
}
/// <summary>
/// Cancels the event to not retry it again.
/// Cancels the event and retries.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="id">The event to enqueue.</param>

1
src/Squidex/Areas/Api/Controllers/Rules/TwitterController.cs

@ -13,6 +13,7 @@ using static CoreTweet.OAuth;
namespace Squidex.Areas.Api.Controllers.Rules
{
[ApiExplorerSettings(IgnoreApi = true)]
public sealed class TwitterController : Controller
{
private readonly TwitterOptions twitterOptions;

2
src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs

@ -123,7 +123,7 @@ namespace Squidex.Areas.Api.Controllers.Statistics
}
/// <summary>
/// Get storage usage in date range.
/// Get asset usage by date.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="fromDate">The from date.</param>

1
src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs

@ -21,6 +21,7 @@ using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Users
{
[ApiModelValidation(true)]
[ApiExplorerSettings(IgnoreApi = true)]
public sealed class UserManagementController : ApiController
{
private readonly UserManager<IdentityUser> userManager;

1
src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs

@ -29,6 +29,7 @@ using Squidex.Shared.Users;
namespace Squidex.Areas.IdentityServer.Controllers.Account
{
[ApiExplorerSettings(IgnoreApi = true)]
public sealed class AccountController : IdentityServerController
{
private readonly SignInManager<IdentityUser> signInManager;

1
src/Squidex/Areas/IdentityServer/Controllers/IdentityServerController.cs

@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Mvc.Filters;
namespace Squidex.Areas.IdentityServer.Controllers
{
[Area("IdentityServer")]
[ApiExplorerSettings(IgnoreApi = true)]
public abstract class IdentityServerController : Controller
{
public override void OnActionExecuting(ActionExecutingContext context)

1
src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs

@ -26,6 +26,7 @@ using Squidex.Shared.Users;
namespace Squidex.Areas.IdentityServer.Controllers.Profile
{
[Authorize]
[ApiExplorerSettings(IgnoreApi = true)]
public sealed class ProfileController : IdentityServerController
{
private readonly SignInManager<IdentityUser> signInManager;

6
src/Squidex/Pipeline/ApiPermissionAttribute.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using IdentityServer4.AccessTokenValidation;
@ -21,6 +22,11 @@ namespace Squidex.Pipeline
{
private readonly string[] permissionIds;
public IEnumerable<string> PermissionIds
{
get { return permissionIds; }
}
public ApiPermissionAttribute(params string[] ids)
{
AuthenticationSchemes = IdentityServerAuthenticationDefaults.AuthenticationScheme;

4
src/Squidex/Pipeline/Swagger/SwaggerHelper.cs → src/Squidex/Pipeline/Swagger/NSwagHelper.cs

@ -19,11 +19,11 @@ using Squidex.Config;
namespace Squidex.Pipeline.Swagger
{
public static class SwaggerHelper
public static class NSwagHelper
{
public static string LoadDocs(string name)
{
var assembly = typeof(SwaggerHelper).GetTypeInfo().Assembly;
var assembly = typeof(NSwagHelper).GetTypeInfo().Assembly;
using (var resourceStream = assembly.GetManifestResourceStream($"Squidex.Docs.{name}.md"))
{
Loading…
Cancel
Save