Browse Source

API documentation.

pull/1/head
Sebastian 9 years ago
parent
commit
5cbb348992
  1. 13
      src/Squidex/Configurations/Identity/IdentityUsage.cs
  2. 20
      src/Squidex/Configurations/Identity/LazyClientStore.cs
  3. 18
      src/Squidex/Configurations/Identity/MyIdentityOptions.cs
  4. 51
      src/Squidex/Configurations/Identity/SwaggerIdentityUsage.cs
  5. 31
      src/Squidex/Configurations/MyUrlsOptions.cs
  6. 123
      src/Squidex/Configurations/Web/SwaggerUsage.cs
  7. 24
      src/Squidex/Modules/Api/Apps/AppClientKeysController.cs
  8. 25
      src/Squidex/Modules/Api/Apps/AppContributorsController.cs
  9. 36
      src/Squidex/Modules/Api/Apps/AppController.cs
  10. 24
      src/Squidex/Modules/Api/Apps/AppLanguagesController.cs
  11. 24
      src/Squidex/Modules/Api/Apps/Models/AppDto.cs
  12. 8
      src/Squidex/Modules/Api/Apps/Models/AssignContributorDto.cs
  13. 6
      src/Squidex/Modules/Api/Apps/Models/ClientKeyCreatedDto.cs
  14. 9
      src/Squidex/Modules/Api/Apps/Models/ClientKeyDto.cs
  15. 3
      src/Squidex/Modules/Api/Apps/Models/ConfigureLanguagesDto.cs
  16. 8
      src/Squidex/Modules/Api/Apps/Models/ContributorDto.cs
  17. 7
      src/Squidex/Modules/Api/Apps/Models/CreateAppDto.cs
  18. 24
      src/Squidex/Modules/Api/Docs/DocsController.cs
  19. 3
      src/Squidex/Modules/Api/EntityCreatedDto.cs
  20. 9
      src/Squidex/Modules/Api/ErrorDto.cs
  21. 11
      src/Squidex/Modules/Api/Languages/LanguagesController.cs
  22. 2
      src/Squidex/Modules/Api/Schemas/SchemaFieldsController.cs
  23. 2
      src/Squidex/Modules/Api/Schemas/SchemasController.cs
  24. 20
      src/Squidex/Modules/Api/Users/Models/UserDto.cs
  25. 18
      src/Squidex/Modules/Api/Users/UsersController.cs
  26. 2
      src/Squidex/Modules/UI/Account/AccountController.cs
  27. 30
      src/Squidex/Pipeline/DescribedResponseTypeAttribute.cs
  28. 5
      src/Squidex/Startup.cs
  29. 13
      src/Squidex/Views/Docs/Docs.cshtml
  30. 2
      src/Squidex/appsettings.Production.json
  31. 6
      src/Squidex/appsettings.json
  32. 3
      src/Squidex/project.json

13
src/Squidex/Configurations/Identity/IdentityUsage.cs

@ -89,17 +89,20 @@ namespace Squidex.Configurations.Identity
{
const string apiScope = Constants.ApiScope;
var options = app.ApplicationServices.GetService<IOptions<MyIdentityOptions>>().Value;
var urlsOptions = app.ApplicationServices.GetService<IOptions<MyUrlsOptions>>().Value;
if (!string.IsNullOrWhiteSpace(options.BaseUrl))
if (!string.IsNullOrWhiteSpace(urlsOptions.BaseUrl))
{
var apiAuthorityUrl = options.BuildUrl(Constants.IdentityPrefix);
var apiAuthorityUrl = urlsOptions.BuildUrl(Constants.IdentityPrefix);
var identityOptions = app.ApplicationServices.GetService<IOptions<MyIdentityOptions>>().Value;
app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions
{
Authority = apiAuthorityUrl,
ScopeName = apiScope,
RequireHttpsMetadata = options.RequiresHttps
ScopeSecret = null,
RequireHttpsMetadata = identityOptions.RequiresHttps
});
}

20
src/Squidex/Configurations/Identity/LazyClientStore.cs

@ -24,14 +24,14 @@ namespace Squidex.Configurations.Identity
private readonly IAppProvider appProvider;
private readonly Dictionary<string, Client> staticClients = new Dictionary<string, Client>(StringComparer.OrdinalIgnoreCase);
public LazyClientStore(IOptions<MyIdentityOptions> identityOptions, IAppProvider appProvider)
public LazyClientStore(IOptions<MyUrlsOptions> urlsOptions, IAppProvider appProvider)
{
Guard.NotNull(identityOptions, nameof(identityOptions));
Guard.NotNull(urlsOptions, nameof(urlsOptions));
Guard.NotNull(appProvider, nameof(appProvider));
this.appProvider = appProvider;
CreateStaticClients(identityOptions);
CreateStaticClients(urlsOptions);
}
public async Task<Client> FindClientByIdAsync(string clientId)
@ -53,9 +53,9 @@ namespace Squidex.Configurations.Identity
return client;
}
private void CreateStaticClients(IOptions<MyIdentityOptions> identityOptions)
private void CreateStaticClients(IOptions<MyUrlsOptions> urlsOptions)
{
foreach (var client in CreateStaticClients(identityOptions.Value))
foreach (var client in CreateStaticClients(urlsOptions.Value))
{
staticClients[client.ClientId] = client;
}
@ -79,7 +79,7 @@ namespace Squidex.Configurations.Identity
};
}
private static IEnumerable<Client> CreateStaticClients(MyIdentityOptions options)
private static IEnumerable<Client> CreateStaticClients(MyUrlsOptions urlsOptions)
{
const string id = Constants.FrontendClient;
@ -89,13 +89,13 @@ namespace Squidex.Configurations.Identity
ClientName = id,
RedirectUris = new List<string>
{
options.BuildUrl("login;"),
options.BuildUrl("identity-server/client-callback-silent/"),
options.BuildUrl("identity-server/client-callback-popup/")
urlsOptions.BuildUrl("login;"),
urlsOptions.BuildUrl("identity-server/client-callback-silent/"),
urlsOptions.BuildUrl("identity-server/client-callback-popup/")
},
PostLogoutRedirectUris = new List<string>
{
options.BuildUrl("logout", false)
urlsOptions.BuildUrl("logout", false)
},
AllowAccessTokensViaBrowser = true,
AllowedGrantTypes = GrantTypes.Implicit,

18
src/Squidex/Configurations/Identity/MyIdentityOptions.cs

@ -6,14 +6,10 @@
// All rights reserved.
// ==========================================================================
using System;
namespace Squidex.Configurations.Identity
{
public sealed class MyIdentityOptions
{
public string BaseUrl { get; set; }
public string DefaultUsername { get; set; }
public string DefaultPassword { get; set; }
@ -23,19 +19,5 @@ namespace Squidex.Configurations.Identity
public string GoogleSecret { get; set; }
public bool RequiresHttps { get; set; }
public string BuildUrl(string path, bool trailingSlash = true)
{
var url = $"{BaseUrl.TrimEnd('/')}/{path.Trim('/')}";
if (trailingSlash &&
url.IndexOf("?", StringComparison.OrdinalIgnoreCase) < 0 &&
url.IndexOf(";", StringComparison.OrdinalIgnoreCase) < 0)
{
url = url + "/";
}
return url;
}
}
}

51
src/Squidex/Configurations/Identity/SwaggerIdentityUsage.cs

@ -0,0 +1,51 @@
// ==========================================================================
// SwaggerIdentityUsage.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using System.Globalization;
using NSwag;
using NSwag.AspNetCore;
using NSwag.CodeGeneration.SwaggerGenerators.WebApi.Processors.Security;
namespace Squidex.Configurations.Identity
{
public static class SwaggerIdentityUsage
{
private const string DescriptionPattern =
@"To retrieve an access token, the client id must make a request to the token url. For example:
$ curl
-X POST '{0}'
-H 'Content-Type: application/x-www-form-urlencoded'
-d 'grant_type=client_credentials&client_id=[APP_NAME]&client_secret=[CLIENT_KEY]'";
public static SwaggerOwinSettings ConfigureIdentity(this SwaggerOwinSettings settings, MyUrlsOptions options)
{
var tokenUrl = options.BuildUrl($"{Constants.IdentityPrefix}/connect/token");
var description = string.Format(CultureInfo.InvariantCulture, DescriptionPattern, tokenUrl);
settings.DocumentProcessors.Add(
new SecurityDefinitionAppender("OAuth2", new SwaggerSecurityScheme
{
TokenUrl = tokenUrl,
Type = SwaggerSecuritySchemeType.OAuth2,
Flow = SwaggerOAuth2Flow.Application,
Scopes = new Dictionary<string, string>
{
{ Constants.ApiScope, "Read and write access to the API" }
},
Description = description
}));
settings.OperationProcessors.Add(new OperationSecurityScopeProcessor("roles"));
return settings;
}
}
}

31
src/Squidex/Configurations/MyUrlsOptions.cs

@ -0,0 +1,31 @@
// ==========================================================================
// MyUrlsOptions.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
namespace Squidex.Configurations
{
public sealed class MyUrlsOptions
{
public string BaseUrl { get; set; }
public string BuildUrl(string path, bool trailingSlash = true)
{
var url = $"{BaseUrl.TrimEnd('/')}/{path.Trim('/')}";
if (trailingSlash &&
url.IndexOf("?", StringComparison.OrdinalIgnoreCase) < 0 &&
url.IndexOf(";", StringComparison.OrdinalIgnoreCase) < 0)
{
url = url + "/";
}
return url;
}
}
}

123
src/Squidex/Configurations/Web/SwaggerUsage.cs

@ -0,0 +1,123 @@
// ==========================================================================
// SwaggerUsage.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using NJsonSchema;
using NJsonSchema.Generation.TypeMappers;
using NSwag;
using NSwag.AspNetCore;
using NSwag.CodeGeneration.SwaggerGenerators.WebApi;
using NSwag.CodeGeneration.SwaggerGenerators.WebApi.Processors;
using NSwag.CodeGeneration.SwaggerGenerators.WebApi.Processors.Contexts;
using Squidex.Configurations.Identity;
using Squidex.Infrastructure;
using Squidex.Modules.Api;
using Squidex.Pipeline;
namespace Squidex.Configurations.Web
{
public static class SwaggerUsage
{
public sealed class DescriptionResponseTypeAttributeProcessor : IOperationProcessor
{
private readonly WebApiToSwaggerGeneratorSettings settings;
public DescriptionResponseTypeAttributeProcessor(WebApiToSwaggerGeneratorSettings settings)
{
this.settings = settings;
}
public bool Process(OperationProcessorContext context)
{
context.OperationDescription.Operation.Responses.Remove("200");
var responseTypes =
context.MethodInfo.GetCustomAttributes<DescribedResponseTypeAttribute>().ToList();
responseTypes.Add(new DescribedResponseTypeAttribute(500, typeof(ErrorDto), "Operation failed."));
foreach (var attribute in responseTypes)
{
var responseType = attribute.Type;
var typeDescription =
JsonObjectTypeDescription.FromType(responseType,
context.MethodInfo.ReturnParameter?.GetCustomAttributes(), settings.DefaultEnumHandling);
var responseCode = attribute.StatusCode.ToString(CultureInfo.InvariantCulture);
var response = new SwaggerResponse { Description = attribute.Description };
if (IsVoidResponse(responseType) == false)
{
response.IsNullableRaw = typeDescription.IsNullable;
response.Schema = context.SwaggerGenerator.GenerateAndAppendSchemaFromType(responseType, typeDescription.IsNullable, null);
}
context.OperationDescription.Operation.Responses[responseCode] = response;
}
return true;
}
private static bool IsVoidResponse(Type returnType)
{
return returnType == null || returnType == typeof(void);
}
}
public static void UseMySwagger(this IApplicationBuilder app)
{
var options = app.ApplicationServices.GetService<IOptions<MyUrlsOptions>>().Value;
var settings =
new SwaggerOwinSettings { Title = "Squidex API Specification" }
.ConfigurePaths()
.ConfigureSchemaSettings()
.ConfigureIdentity(options);
app.UseSwagger(typeof(SwaggerUsage).GetTypeInfo().Assembly, settings);
}
private static SwaggerOwinSettings ConfigurePaths(this SwaggerOwinSettings settings)
{
settings.SwaggerRoute = $"{Constants.ApiPrefix}/swagger/v1/swagger.json";
settings.PostProcess = document =>
{
document.BasePath = Constants.ApiPrefix;
};
settings.MiddlewareBasePath = Constants.ApiPrefix;
return settings;
}
private static SwaggerOwinSettings ConfigureSchemaSettings(this SwaggerOwinSettings settings)
{
settings.DefaultEnumHandling = EnumHandling.String;
settings.DefaultPropertyNameHandling = PropertyNameHandling.CamelCase;
settings.TypeMappers = new List<ITypeMapper>
{
new PrimitiveTypeMapper(typeof(Language), s => s.Type = JsonObjectType.String)
};
settings.OperationProcessors.Add(new DescriptionResponseTypeAttributeProcessor(settings));
return settings;
}
}
}

24
src/Squidex/Modules/Api/Apps/AppClientKeysController.cs

@ -10,6 +10,7 @@ using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Infrastructure.Reflection;
using Squidex.Modules.Api.Apps.Models;
@ -35,8 +36,17 @@ namespace Squidex.Modules.Api.Apps
this.keyGenerator = keyGenerator;
}
/// <summary>
/// Get app client keys.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <remarks>
/// Gets all configured client keys for the app with the specified name.
/// </remarks>
[HttpGet]
[Route("apps/{app}/client-keys/")]
[SwaggerTags("Apps")]
[DescribedResponseType(200, typeof(ClientKeyDto[]), "Client keys returned..")]
public async Task<IActionResult> GetContributors(string app)
{
var entity = await appProvider.FindAppByNameAsync(app);
@ -51,15 +61,25 @@ namespace Squidex.Modules.Api.Apps
return Ok(model);
}
/// <summary>
/// Create new client key.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <remarks>
/// Create a new client key for the app with the specified name.
/// The client key is auto generated on the server and returned.
/// </remarks>
[HttpPost]
[Route("apps/{app}/client-keys/")]
public async Task<IActionResult> PostClientKey()
[SwaggerTags("Apps")]
[DescribedResponseType(201, typeof(ClientKeyCreatedDto[]), "Client key created.")]
public async Task<IActionResult> PostClientKey(string app)
{
var clientKey = keyGenerator.GenerateKey();
await CommandBus.PublishAsync(new CreateClientKey { ClientKey = clientKey });
return Ok(new ClientKeyCreatedDto { ClientKey = clientKey });
return StatusCode(201, new ClientKeyCreatedDto { ClientKey = clientKey });
}
}
}

25
src/Squidex/Modules/Api/Apps/AppContributorsController.cs

@ -10,12 +10,12 @@ using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Infrastructure.Reflection;
using Squidex.Modules.Api.Apps.Models;
using Squidex.Pipeline;
using Squidex.Read.Apps.Services;
using Squidex.Write.Apps;
using Squidex.Write.Apps.Commands;
namespace Squidex.Modules.Api.Apps
@ -33,8 +33,13 @@ namespace Squidex.Modules.Api.Apps
this.appProvider = appProvider;
}
/// <summary>
/// Get contributors for the app.
/// </summary>
/// <param name="app">The name of the app.</param>
[HttpGet]
[Route("apps/{app}/contributors/")]
[SwaggerTags("Apps")]
public async Task<IActionResult> GetContributors(string app)
{
var entity = await appProvider.FindAppByNameAsync(app);
@ -49,18 +54,32 @@ namespace Squidex.Modules.Api.Apps
return Ok(model);
}
/// <summary>
/// Assign contributor to the app.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="model">Contributor object that needs to be added to the app.</param>
[HttpPost]
[Route("apps/{app}/contributors/")]
public async Task<IActionResult> PostContributor([FromBody] AssignContributorDto model)
[SwaggerTags("Apps")]
[DescribedResponseType(400, typeof(ErrorDto), "User is already contributed to the app or user not found.")]
public async Task<IActionResult> PostContributor(string app, [FromBody] AssignContributorDto model)
{
await CommandBus.PublishAsync(SimpleMapper.Map(model, new AssignContributor()));
return Ok();
}
/// <summary>
/// Removes contributor from app.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="contributorId"></param>
[HttpDelete]
[Route("apps/{app}/contributors/{contributorId}/")]
public async Task<IActionResult> PutContributor(string contributorId)
[SwaggerTags("Apps")]
[DescribedResponseType(400, typeof(ErrorDto), "User is not a contributor of the app.")]
public async Task<IActionResult> DeleteContributor(string app, string contributorId)
{
await CommandBus.PublishAsync(new RemoveContributor { ContributorId = contributorId });

36
src/Squidex/Modules/Api/Apps/AppController.cs

@ -7,11 +7,11 @@
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.Security;
@ -24,6 +24,7 @@ namespace Squidex.Modules.Api.Apps
{
[Authorize]
[ApiExceptionFilter]
[SwaggerTag("Apps", Description = "Manages and configures apps.")]
public class AppController : ControllerBase
{
private readonly IAppRepository appRepository;
@ -34,32 +35,55 @@ namespace Squidex.Modules.Api.Apps
this.appRepository = appRepository;
}
/// <summary>
/// Gets your apps.
/// </summary>
/// <remarks>
/// You can only retrieve the list of apps when you are authenticated as a user (OpenID implicit flow).
/// You will retrieve all apps, where you are assigned as a contributor.
/// </remarks>
[HttpGet]
[Route("apps/")]
public async Task<List<ListAppDto>> Query()
[SwaggerTags("Apps")]
[DescribedResponseType(200, typeof(AppDto[]), "Apps returned")]
public async Task<IActionResult> GetApps()
{
var subject = HttpContext.User.OpenIdSubject();
var schemas = await appRepository.QueryAllAsync(subject);
return schemas.Select(s =>
var models = schemas.Select(s =>
{
var dto = SimpleMapper.Map(s, new ListAppDto());
var dto = SimpleMapper.Map(s, new AppDto());
dto.Permission = s.Contributors.Single(x => x.ContributorId == subject).Permission;
return dto;
}).ToList();
return Ok(models);
}
/// <summary>
/// Create a new app.
/// </summary>
/// <param name="model">The app object that needs to be added to squided.</param>
/// <remarks>
/// You can only create an app when you are authenticated as a user (OpenID implicit flow).
/// You will be assigned as owner of the new app automatically.
/// </remarks>
[HttpPost]
[Route("apps/")]
public async Task<IActionResult> Create([FromBody] CreateAppDto model)
[SwaggerTags("Apps")]
[DescribedResponseType(201, typeof(EntityCreatedDto), "App created.")]
[DescribedResponseType(400, typeof(ErrorDto), "App object is not valid.")]
[DescribedResponseType(409, typeof(ErrorDto), "App name already in use.")]
public async Task<IActionResult> PostApp([FromBody] CreateAppDto model)
{
var command = SimpleMapper.Map(model, new CreateApp { AggregateId = Guid.NewGuid() });
await CommandBus.PublishAsync(command);
return CreatedAtAction("Query", new EntityCreatedDto { Id = command.AggregateId });
return CreatedAtAction(nameof(GetApps), new EntityCreatedDto { Id = command.AggregateId });
}
}
}

24
src/Squidex/Modules/Api/Apps/AppLanguagesController.cs

@ -10,6 +10,7 @@ using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Infrastructure.Reflection;
using Squidex.Modules.Api.Apps.Models;
@ -32,9 +33,15 @@ namespace Squidex.Modules.Api.Apps
this.appProvider = appProvider;
}
/// <summary>
/// Get app languages.
/// </summary>
/// <param name="app">The name of the app.</param>
[HttpGet]
[Route("apps/{app}/languages/")]
public async Task<IActionResult> GetContributors(string app)
[SwaggerTags("Apps")]
[DescribedResponseType(200, typeof(AppDto[]), "Language configuration returned.")]
public async Task<IActionResult> GetLanguages(string app)
{
var entity = await appProvider.FindAppByNameAsync(app);
@ -48,9 +55,22 @@ namespace Squidex.Modules.Api.Apps
return Ok(model);
}
/// <summary>
/// Configures the app languages.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="model">The language configuration for the app.</param>
/// <remarks>
/// The ordering of the languages matterns: When you retrieve a content with a localized content squidex tries
/// to resolve the correct language for these properties. When there is no value for a property in the specified language,
/// the previous languages from languages list is uses as a fallback.
/// </remarks>
[HttpPost]
[Route("apps/{app}/languages/")]
public async Task<IActionResult> PostLanguages([FromBody] ConfigureLanguagesDto model)
[SwaggerTags("Apps")]
[DescribedResponseType(400, typeof(ErrorDto[]), "Language configuration is empty.")]
[DescribedResponseType(400, typeof(ErrorDto[]), "Language configuration contains an invalid language.")]
public async Task<IActionResult> PostLanguages(string app, [FromBody] ConfigureLanguagesDto model)
{
await CommandBus.PublishAsync(SimpleMapper.Map(model, new ConfigureLanguages()));

24
src/Squidex/Modules/Api/Apps/Models/ListAppDto.cs → src/Squidex/Modules/Api/Apps/Models/AppDto.cs

@ -7,22 +7,40 @@
// ==========================================================================
using System;
using System.ComponentModel.DataAnnotations;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Squidex.Core.Apps;
namespace Squidex.Modules.Api.Apps.Models
{
public sealed class ListAppDto
public sealed class AppDto
{
public Guid Id { get; set; }
/// <summary>
/// The name of the app.
/// </summary>
[Required]
[RegularExpression("^[a-z0-9]+(\\-[a-z0-9]+)*$")]
public string Name { get; set; }
/// <summary>
/// The name of the app.
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// The date and time when the app has been created.
/// </summary>
public DateTime Created { get; set; }
/// <summary>
/// The date and time when the app has been modified last.
/// </summary>
public DateTime LastModified { get; set; }
/// <summary>
/// The permission level of the user.
/// </summary>
[JsonConverter(typeof(StringEnumConverter))]
public PermissionLevel Permission { get; set; }
}

8
src/Squidex/Modules/Api/Apps/Models/AssignContributorDto.cs

@ -9,13 +9,21 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Squidex.Core.Apps;
using System.ComponentModel.DataAnnotations;
namespace Squidex.Modules.Api.Apps.Models
{
public class AssignContributorDto
{
/// <summary>
/// The id of the user to add to the app (GUID).
/// </summary>
[Required]
public string ContributorId { get; set; }
/// <summary>
/// The permission level as a contributor.
/// </summary>
[JsonConverter(typeof(StringEnumConverter))]
public PermissionLevel Permission { get; set; }
}

6
src/Squidex/Modules/Api/Apps/Models/ClientKeyCreatedDto.cs

@ -6,10 +6,16 @@
// All rights reserved.
// ==========================================================================
using System.ComponentModel.DataAnnotations;
namespace Squidex.Modules.Api.Apps.Models
{
public sealed class ClientKeyCreatedDto
{
/// <summary>
/// The created client key.
/// </summary>
[Required]
public string ClientKey { get; set; }
}
}

9
src/Squidex/Modules/Api/Apps/Models/ClientKeyDto.cs

@ -7,13 +7,22 @@
// ==========================================================================
using System;
using System.ComponentModel.DataAnnotations;
namespace Squidex.Modules.Api.Apps.Models
{
public sealed class ClientKeyDto
{
/// <summary>
/// The client key.
/// </summary>
[Required]
public string ClientKey { get; set; }
/// <summary>
/// The date and time when the client key expires.
/// </summary>
[Required]
public DateTime ExpiresUtc { get; set; }
}
}

3
src/Squidex/Modules/Api/Apps/Models/ConfigureLanguagesDto.cs

@ -13,6 +13,9 @@ namespace Squidex.Modules.Api.Apps.Models
{
public class ConfigureLanguagesDto
{
/// <summary>
/// The list of languages to configure the app.
/// </summary>
public List<Language> Languages { get; set; }
}
}

8
src/Squidex/Modules/Api/Apps/Models/ContributorDto.cs

@ -6,6 +6,7 @@
// All rights reserved.
// ==========================================================================
using System.ComponentModel.DataAnnotations;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Squidex.Core.Apps;
@ -14,8 +15,15 @@ namespace Squidex.Modules.Api.Apps.Models
{
public sealed class ContributorDto
{
/// <summary>
/// The id of the user that contributes to the app (GUID).
/// </summary>
[Required]
public string ContributorId { get; set; }
/// <summary>
/// The permission level as a contributor.
/// </summary>
[JsonConverter(typeof(StringEnumConverter))]
public PermissionLevel Permission { get; set; }
}

7
src/Squidex/Modules/Api/Apps/Models/CreateAppDto.cs

@ -6,10 +6,17 @@
// All rights reserved.
// ==========================================================================
using System.ComponentModel.DataAnnotations;
namespace Squidex.Modules.Api.Apps.Models
{
public sealed class CreateAppDto
{
/// <summary>
/// The new name of the app.
/// </summary>
[Required]
[RegularExpression("^[a-z0-9]+(\\-[a-z0-9]+)*$")]
public string Name { get; set; }
}
}

24
src/Squidex/Modules/Api/Docs/DocsController.cs

@ -0,0 +1,24 @@
// ==========================================================================
// DocsController.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
namespace Squidex.Modules.Api.Docs
{
[SwaggerIgnore]
public sealed class DocsController : Controller
{
[HttpGet]
[Route("docs/")]
public IActionResult Docs()
{
return View();
}
}
}

3
src/Squidex/Modules/Api/EntityCreatedDto.cs

@ -9,6 +9,9 @@ namespace Squidex.Modules.Api
{
public class EntityCreatedDto
{
/// <summary>
/// Id of the created entity.
/// </summary>
public object Id { get; set; }
}
}

9
src/Squidex/Modules/Api/ErrorDto.cs

@ -12,11 +12,20 @@ namespace Squidex.Modules.Api
{
public sealed class ErrorDto
{
/// <summary>
/// Error message.
/// </summary>
[Required]
public string Message { get; set; }
/// <summary>
/// Detailed error messages.
/// </summary>
public string[] Details { get; set; }
/// <summary>
/// Status code of the http response.
/// </summary>
public int? StatusCode { get; set; } = 400;
}
}

11
src/Squidex/Modules/Api/Languages/LanguagesController.cs

@ -9,6 +9,7 @@
using System.Linq;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Reflection;
using Squidex.Pipeline;
@ -17,10 +18,20 @@ namespace Squidex.Modules.Api.Languages
{
[Authorize]
[ApiExceptionFilter]
[SwaggerTag("Languages", Description = "Readonly API to the supported langauges.")]
public class LanguagesController : Controller
{
/// <summary>
/// Get supported languages.
/// </summary>
/// <remarks>
/// Provide a list of supported langauges code, following the ISO2Code standard.
/// </remarks>
/// <response code="200">Language codes returned.</response>
[HttpGet]
[Route("languages/")]
[SwaggerTags("Languages")]
[DescribedResponseType(200, typeof(string[]), "Supported languages returned.")]
public IActionResult GetLanguages()
{
var model = Language.AllLanguages.Select(x => SimpleMapper.Map(x, new LanguageDto())).ToList();

2
src/Squidex/Modules/Api/Schemas/SchemaFieldsController.cs

@ -9,6 +9,7 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Infrastructure.Reflection;
using Squidex.Modules.Api.Schemas.Models;
@ -20,6 +21,7 @@ namespace Squidex.Modules.Api.Schemas
[Authorize]
[ApiExceptionFilter]
[ServiceFilter(typeof(AppFilterAttribute))]
[SwaggerIgnore]
public class SchemasFieldsController : ControllerBase
{
public SchemasFieldsController(ICommandBus commandBus)

2
src/Squidex/Modules/Api/Schemas/SchemasController.cs

@ -12,6 +12,7 @@ using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Infrastructure.Reflection;
using Squidex.Modules.Api.Schemas.Models;
@ -25,6 +26,7 @@ namespace Squidex.Modules.Api.Schemas
[Authorize]
[ApiExceptionFilter]
[ServiceFilter(typeof(AppFilterAttribute))]
[SwaggerIgnore]
public class SchemasController : ControllerBase
{
private readonly ISchemaRepository schemaRepository;

20
src/Squidex/Modules/Api/Users/Models/UserDto.cs

@ -6,16 +6,34 @@
// All rights reserved.
// ==========================================================================
using System.ComponentModel.DataAnnotations;
namespace Squidex.Modules.Api.Users.Models
{
public sealed class UserDto
{
/// <summary>
/// The id of the user. Unique value.
/// </summary>
[Required]
public string Id { get; set; }
/// <summary>
/// The email of the user. Unique value.
/// </summary>
[Required]
public string Email { get; set; }
/// <summary>
/// The url to the profile picture of the user.
/// </summary>
[Required]
public string PictureUrl { get; set; }
/// <summary>
/// The display name (usually first name and last name) of the user.
/// </summary>
[Required]
public string DisplayName { get; set; }
}
}

18
src/Squidex/Modules/Api/Users/UsersController.cs

@ -10,6 +10,7 @@ using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using Squidex.Infrastructure.Reflection;
using Squidex.Modules.Api.Users.Models;
using Squidex.Pipeline;
@ -19,6 +20,7 @@ namespace Squidex.Modules.Api.Users
{
[Authorize]
[ApiExceptionFilter]
[SwaggerTag("Users", Description = "Readonly API to retrieve information about squidex users.")]
public class UsersController : Controller
{
private readonly IUserRepository userRepository;
@ -28,8 +30,17 @@ namespace Squidex.Modules.Api.Users
this.userRepository = userRepository;
}
/// <summary>
/// Get users by query.
/// </summary>
/// <param name="query">The query to search the user by email address. Case invariant.</param>
/// <remarks>
/// Search the user by query that contains the email address or the part of the email address.
/// </remarks>
[HttpGet]
[Route("users")]
[SwaggerTags("Users")]
[DescribedResponseType(200, typeof(UserDto[]), "Users returned.")]
public async Task<IActionResult> GetUsers(string query)
{
var entities = await userRepository.FindUsersByQuery(query ?? string.Empty);
@ -39,8 +50,15 @@ namespace Squidex.Modules.Api.Users
return Ok(model);
}
/// <summary>
/// Get user by id.
/// </summary>
/// <param name="id">The id of the user (GUID).</param>
[HttpGet]
[Route("users/{id}/")]
[SwaggerTags("Users")]
[DescribedResponseType(200, typeof(UserDto), "User found.")]
[DescribedResponseType(404, typeof(void), "User not found.")]
public async Task<IActionResult> GetUser(string id)
{
var entity = await userRepository.FindUserByIdAsync(id);

2
src/Squidex/Modules/UI/Account/AccountController.cs

@ -14,6 +14,7 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.MongoDB;
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using Squidex.Infrastructure.Security;
// ReSharper disable RedundantIfElseBlock
@ -21,6 +22,7 @@ using Squidex.Infrastructure.Security;
namespace Squidex.Modules.UI.Account
{
[SwaggerIgnore]
public sealed class AccountController : Controller
{
private readonly SignInManager<IdentityUser> signInManager;

30
src/Squidex/Pipeline/DescribedResponseTypeAttribute.cs

@ -0,0 +1,30 @@
// ==========================================================================
// DescribedResponseTypeAttribute.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using Microsoft.AspNetCore.Mvc;
namespace Squidex.Pipeline
{
public sealed class DescribedResponseTypeAttribute : ProducesResponseTypeAttribute
{
public string Description { get; }
public DescribedResponseTypeAttribute(int statusCode, string description = null)
: base(typeof(void), statusCode)
{
Description = description;
}
public DescribedResponseTypeAttribute(int statusCode, Type type, string description = null)
: base(type, statusCode)
{
Description = description;
}
}
}

5
src/Squidex/Startup.cs

@ -74,6 +74,8 @@ namespace Squidex
Configuration.GetSection("stores:mongoDb"));
services.Configure<MyEventStoreOptions>(
Configuration.GetSection("stores:eventStore"));
services.Configure<MyUrlsOptions>(
Configuration.GetSection("urls"));
services.Configure<MyIdentityOptions>(
Configuration.GetSection("identity"));
@ -98,7 +100,7 @@ namespace Squidex
{
app.UseMiddleware<SingleUrlsMiddleware>();
}
MapAndUseIdentity(app);
MapAndUseApi(app);
MapAndUseFrontend(app);
@ -137,6 +139,7 @@ namespace Squidex
appApi.UseDeveloperExceptionPage();
}
appApi.UseMySwagger();
appApi.UseMyApiProtection();
appApi.MapWhen(x => !IsIdentityRequest(x), mvcApp =>

13
src/Squidex/Views/Docs/Docs.cshtml

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<title>API Docs</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<redoc spec-url="@Url.Content("~/swagger/v1/swagger.json")"></redoc>
<script src="https://rebilly.github.io/ReDoc/releases/latest/redoc.min.js"></script>
</body>
</html>

2
src/Squidex/appsettings.Production.json

@ -1,5 +1,5 @@
{
"identity": {
"urls": {
"baseUrl": "https://squidex.io"
}
}

6
src/Squidex/appsettings.json

@ -1,4 +1,7 @@
{
"urls": {
"baseUrl": "http://localhost:5000"
},
"stores": {
"mongoDb": {
"connectionString": "mongodb://localhost",
@ -14,7 +17,6 @@
},
"identity": {
"googleClient": "900148737461-7e3hv5425idg7ql9vlhfubo8hjm7d94h.apps.googleusercontent.com",
"googleSecret": "J1yE5kbw_NsAc759pGSU-WPn",
"baseUrl": "http://localhost:5000"
"googleSecret": "J1yE5kbw_NsAc759pGSU-WPn"
}
}

3
src/Squidex/project.json

@ -29,6 +29,7 @@
"type": "platform"
},
"MongoDB.Driver": "2.4.0-beta1",
"NSwag.AspNetCore": "7.2.0",
"OpenCover": "4.6.519",
"ReportGenerator": "2.5.0-beta1",
"Squidex.Core": "1.0.0-*",
@ -56,7 +57,7 @@
"buildOptions": {
"emitEntryPoint": true,
"preserveCompilationContext": true,
"xmlDoc": false,
"xmlDoc": true,
"nowarn": [ "1591", "1573", "1572" ],
"embed": {
"include": [

Loading…
Cancel
Save