Browse Source

Merge pull request #336 from Squidex/nswag

Nswag
pull/337/head
Sebastian Stehle 7 years ago
committed by GitHub
parent
commit
2d353537a9
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      Dockerfile
  2. 2
      Dockerfile.build
  3. 6
      Squidex.ruleset
  4. 10
      extensions/Squidex.Extensions/Squidex.Extensions.csproj
  5. 58
      libs/Dockerfile
  6. 6
      src/Squidex.Domain.Apps.Core.Model/FodyWeavers.xml
  7. 26
      src/Squidex.Domain.Apps.Core.Model/FodyWeavers.xsd
  8. 2
      src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj
  9. 4
      src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj
  10. 2
      src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj
  11. 12
      src/Squidex.Domain.Apps.Entities/Apps/Diagnostics/OrleansAppsHealthCheck.cs
  12. 2
      src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj
  13. 4
      src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj
  14. 4
      src/Squidex.Domain.Users/Squidex.Domain.Users.csproj
  15. 2
      src/Squidex.Infrastructure.Azure/Squidex.Infrastructure.Azure.csproj
  16. 11
      src/Squidex.Infrastructure.GetEventStore/Diagnostics/GetEventStoreHealthCheck.cs
  17. 13
      src/Squidex.Infrastructure.MongoDb/Diagnostics/MongoDBHealthCheck.cs
  18. 29
      src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsageRepository.cs
  19. 16
      src/Squidex.Infrastructure/Diagnostics/GCHealthCheck.cs
  20. 27
      src/Squidex.Infrastructure/Diagnostics/HealthCheckResult.cs
  21. 16
      src/Squidex.Infrastructure/Diagnostics/HealthCheckScopes.cs
  22. 20
      src/Squidex.Infrastructure/Diagnostics/IHealthCheck.cs
  23. 13
      src/Squidex.Infrastructure/Diagnostics/OrleansHealthCheck.cs
  24. 10
      src/Squidex.Infrastructure/HashSet.cs
  25. 9
      src/Squidex.Infrastructure/Squidex.Infrastructure.csproj
  26. 2
      src/Squidex.Infrastructure/UsageTracking/IUsageRepository.cs
  27. 33
      src/Squidex/Areas/Api/Config/Swagger/FixProcessor.cs
  28. 21
      src/Squidex/Areas/Api/Config/Swagger/ODataQueryParamsProcessor.cs
  29. 30
      src/Squidex/Areas/Api/Config/Swagger/ScopesProcessor.cs
  30. 48
      src/Squidex/Areas/Api/Config/Swagger/SecurityProcessor.cs
  31. 15
      src/Squidex/Areas/Api/Config/Swagger/SwaggerExtensions.cs
  32. 92
      src/Squidex/Areas/Api/Config/Swagger/SwaggerServices.cs
  33. 8
      src/Squidex/Areas/Api/Config/Swagger/TagByGroupNameProcessor.cs
  34. 41
      src/Squidex/Areas/Api/Config/Swagger/ThemeProcessor.cs
  35. 9
      src/Squidex/Areas/Api/Config/Swagger/XmlResponseTypesProcessor.cs
  36. 8
      src/Squidex/Areas/Api/Config/Swagger/XmlTagProcessor.cs
  37. 14
      src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs
  38. 2
      src/Squidex/Areas/Api/Controllers/Contents/ContentSwaggerController.cs
  39. 7
      src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs
  40. 40
      src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaSwaggerGenerator.cs
  41. 43
      src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasSwaggerGenerator.cs
  42. 54
      src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionProcessor.cs
  43. 4
      src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs
  44. 6
      src/Squidex/Areas/Api/Controllers/Rules/TwitterController.cs
  45. 2
      src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs
  46. 5
      src/Squidex/Config/Domain/EntitiesServices.cs
  47. 7
      src/Squidex/Config/Domain/EventStoreServices.cs
  48. 12
      src/Squidex/Config/Domain/InfrastructureServices.cs
  49. 6
      src/Squidex/Config/Domain/LoggingExtensions.cs
  50. 4
      src/Squidex/Config/Domain/StoreServices.cs
  51. 55
      src/Squidex/Config/Web/WebExtensions.cs
  52. 4
      src/Squidex/Config/Web/WebServices.cs
  53. 2
      src/Squidex/Dockerfile
  54. 6
      src/Squidex/Pipeline/ApiPermissionAttribute.cs
  55. 113
      src/Squidex/Pipeline/Diagnostics/HealthCheckMiddleware.cs
  56. 38
      src/Squidex/Pipeline/Swagger/NSwagHelper.cs
  57. 2
      src/Squidex/Program.cs
  58. 63
      src/Squidex/Squidex.csproj
  59. 9
      src/Squidex/web.config
  60. 4
      tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj
  61. 4
      tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj
  62. 4
      tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj
  63. 8
      tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj
  64. 8
      tests/Squidex.Tests/Squidex.Tests.csproj
  65. 2
      tools/GenerateLanguages/GenerateLanguages.csproj
  66. 2
      tools/Migrate_00/Migrate_00.csproj

4
Dockerfile

@ -1,7 +1,7 @@
#
# Stage 1, Prebuild
#
FROM squidex/dotnet:2.1-sdk-chromium-phantomjs-node as builder
FROM squidex/dotnet:2.2-sdk-chromium-phantomjs-node as builder
COPY src/Squidex/package.json /tmp/package.json
@ -33,7 +33,7 @@ RUN dotnet publish src/Squidex/Squidex.csproj --output /out/alpine --configurati
#
# Stage 2, Build runtime
#
FROM microsoft/dotnet:2.1-runtime-deps-alpine
FROM microsoft/dotnet:2.2-runtime-deps-alpine
# Default AspNetCore directory
WORKDIR /app

2
Dockerfile.build

@ -1,4 +1,4 @@
FROM squidex/dotnet:2.1-sdk-chromium-phantomjs-node as builder
FROM squidex/dotnet:2.2-sdk-chromium-phantomjs-node as builder
COPY src/Squidex/package.json /tmp/package.json

6
Squidex.ruleset

@ -84,4 +84,10 @@
<Rule Id="IDE0032" Action="None" />
<Rule Id="IDE0042" Action="None" />
</Rules>
<Rules AnalyzerId="Microsoft.AspNetCore.Mvc.Api.Analyzers" RuleNamespace="Microsoft.AspNetCore.Mvc.Api.Analyzers">
<Rule Id="API1000" Action="Error" />
<Rule Id="API1001" Action="Error" />
<Rule Id="API1002" Action="Error" />
<Rule Id="API1003" Action="Error" />
</Rules>
</RuleSet>

10
extensions/Squidex.Extensions/Squidex.Extensions.csproj

@ -10,19 +10,19 @@
<ProjectReference Include="..\..\src\Squidex.Domain.Apps.Core.Operations\Squidex.Domain.Apps.Core.Operations.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Algolia.Search" Version="5.2.0" />
<PackageReference Include="Algolia.Search" Version="5.2.1" />
<PackageReference Include="CoreTweet" Version="1.0.0.483" />
<PackageReference Include="Elasticsearch.Net" Version="6.4.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="2.1.1" />
<PackageReference Include="Microsoft.OData.Core" Version="7.5.1" />
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
<PackageReference Include="Microsoft.Extensions.Http" Version="2.2.0" />
<PackageReference Include="Microsoft.OData.Core" Version="7.5.2" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
<PackageReference Include="NodaTime" Version="2.4.2" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="1.5.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="4.5.0" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
<PackageReference Include="WindowsAzure.Storage" Version="9.3.2" />
<PackageReference Include="WindowsAzure.Storage" Version="9.3.3" />
</ItemGroup>
<PropertyGroup>
<CodeAnalysisRuleSet>..\..\Squidex.ruleset</CodeAnalysisRuleSet>

58
libs/Dockerfile

@ -0,0 +1,58 @@
FROM microsoft/dotnet:2.2-sdk
# Install runtime dependencies
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates bzip2 libfontconfig \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Install official PhantomJS release
RUN set -x \
&& apt-get update \
&& apt-get install -y --no-install-recommends \
&& mkdir /srv/var \
&& mkdir /tmp/phantomjs \
# Download Phantom JS
&& curl -L https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2 | tar -xj --strip-components=1 -C /tmp/phantomjs \
# Copy binaries only
&& mv /tmp/phantomjs/bin/phantomjs /usr/local/bin \
# Create symbol link
# Clean up
&& apt-get autoremove -y \
&& apt-get clean all \
&& rm -rf /tmp/* /var/lib/apt/lists/*
RUN phantomjs --version
# Install Node
ENV NODE_VERSION 8.9.4
ENV NODE_DOWNLOAD_SHA 21fb4690e349f82d708ae766def01d7fec1b085ce1f5ab30d9bda8ee126ca8fc
RUN curl -SL "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.gz" --output nodejs.tar.gz \
&& echo "$NODE_DOWNLOAD_SHA nodejs.tar.gz" | sha256sum -c - \
&& tar -xzf "nodejs.tar.gz" -C /usr/local --strip-components=1 \
&& rm nodejs.tar.gz \
&& ln -s /usr/local/bin/node /usr/local/bin/nodejs
# Install Google Chrome
# See https://crbug.com/795759
RUN apt-get update && apt-get install -yq libgconf-2-4
# Install latest chrome dev package and fonts to support major charsets (Chinese, Japanese, Arabic, Hebrew, Thai and a few others)
RUN apt-get update && apt-get install -y wget --no-install-recommends \
&& wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
&& sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
&& apt-get update \
&& apt-get install -y google-chrome-unstable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst ttf-freefont \
--no-install-recommends \
&& rm -rf /var/lib/apt/lists/* \
&& apt-get autoremove -y \
&& rm -rf /src/*.deb
# It's a good idea to use dumb-init to help prevent zombie chrome processes.
ADD https://github.com/Yelp/dumb-init/releases/download/v1.2.0/dumb-init_1.2.0_amd64 /usr/local/bin/dumb-init
RUN chmod +x /usr/local/bin/dumb-init
# Install puppeteer so it's available in the container.
RUN npm i puppeteer

6
src/Squidex.Domain.Apps.Core.Model/FodyWeavers.xml

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8" ?>
<Weavers>
<Freezable/>
<?xml version="1.0" encoding="utf-8"?>
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<Freezable />
</Weavers>

26
src/Squidex.Domain.Apps.Core.Model/FodyWeavers.xsd

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
<xs:element name="Weavers">
<xs:complexType>
<xs:all>
<xs:element name="Freezable" minOccurs="0" maxOccurs="1" type="xs:anyType" />
</xs:all>
<xs:attribute name="VerifyAssembly" type="xs:boolean">
<xs:annotation>
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="VerifyIgnoreCodes" type="xs:string">
<xs:annotation>
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="GenerateXsd" type="xs:boolean">
<xs:annotation>
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:schema>

2
src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj

@ -8,7 +8,7 @@
<DebugSymbols>True</DebugSymbols>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Fody" Version="3.2.17">
<PackageReference Include="Fody" Version="3.3.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>

4
src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj

@ -15,8 +15,8 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Jint" Version="2.11.58" />
<PackageReference Include="Microsoft.OData.Core" Version="7.5.1" />
<PackageReference Include="NJsonSchema" Version="9.12.2" />
<PackageReference Include="Microsoft.OData.Core" Version="7.5.2" />
<PackageReference Include="NJsonSchema" Version="9.13.2" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="1.5.0" />

2
src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj

@ -15,7 +15,7 @@
<ProjectReference Include="..\Squidex.Domain.Apps.Entities\Squidex.Domain.Apps.Entities.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.OData.Core" Version="7.5.1" />
<PackageReference Include="Microsoft.OData.Core" Version="7.5.2" />
<PackageReference Include="MongoDB.Driver" Version="2.7.2" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" PrivateAssets="all" />

12
src/Squidex.Domain.Apps.Entities/Apps/Diagnostics/OrleansAppsHealthCheck.cs

@ -5,13 +5,12 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Orleans;
using Squidex.Domain.Apps.Entities.Apps.Indexes;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Diagnostics;
using Squidex.Infrastructure.Orleans;
namespace Squidex.Domain.Apps.Entities.Apps.Diagnostics
@ -20,11 +19,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.Diagnostics
{
private readonly IAppsByNameIndex index;
public IEnumerable<string> Scopes
{
get { yield return HealthCheckScopes.Cluster; }
}
public OrleansAppsHealthCheck(IGrainFactory grainFactory)
{
Guard.NotNull(grainFactory, nameof(grainFactory));
@ -32,11 +26,11 @@ namespace Squidex.Domain.Apps.Entities.Apps.Diagnostics
index = grainFactory.GetGrain<IAppsByNameIndex>(SingleGrain.Id);
}
public async Task<HealthCheckResult> CheckHealthAsync(CancellationToken cancellationToken = default(CancellationToken))
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default(CancellationToken))
{
await index.CountAsync();
return new HealthCheckResult(true);
return HealthCheckResult.Healthy("Orleans must establish communication.");
}
}
}

2
src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj

@ -11,7 +11,7 @@
<ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="2.1.2" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="2.2.0" />
<PackageReference Include="NodaTime" Version="2.4.2" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" PrivateAssets="all" />

4
src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj

@ -13,8 +13,8 @@
<ProjectReference Include="..\Squidex.Shared\Squidex.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="IdentityServer4" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.1.6" />
<PackageReference Include="IdentityServer4" Version="2.3.2" />
<PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.2.0" />
<PackageReference Include="Microsoft.Win32.Registry" Version="4.5.0" />
<PackageReference Include="MongoDB.Driver" Version="2.7.2" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />

4
src/Squidex.Domain.Users/Squidex.Domain.Users.csproj

@ -11,8 +11,8 @@
<ProjectReference Include="..\Squidex.Shared\Squidex.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.1.6" />
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="2.1.6" />
<PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="2.2.0" />
<PackageReference Include="Microsoft.Win32.Registry" Version="4.5.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="SharpPwned.NET" Version="1.0.8" />

2
src/Squidex.Infrastructure.Azure/Squidex.Infrastructure.Azure.csproj

@ -6,7 +6,7 @@
<ItemGroup>
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" PrivateAssets="all" />
<PackageReference Include="WindowsAzure.Storage" Version="9.3.2" />
<PackageReference Include="WindowsAzure.Storage" Version="9.3.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />

11
src/Squidex.Infrastructure.GetEventStore/Diagnostics/GetEventStoreHealthCheck.cs

@ -5,10 +5,10 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using EventStore.ClientAPI;
using Microsoft.Extensions.Diagnostics.HealthChecks;
namespace Squidex.Infrastructure.Diagnostics
{
@ -16,11 +16,6 @@ namespace Squidex.Infrastructure.Diagnostics
{
private readonly IEventStoreConnection connection;
public IEnumerable<string> Scopes
{
get { yield return HealthCheckScopes.Node; }
}
public GetEventStoreHealthCheck(IEventStoreConnection connection)
{
Guard.NotNull(connection, nameof(connection));
@ -28,11 +23,11 @@ namespace Squidex.Infrastructure.Diagnostics
this.connection = connection;
}
public async Task<HealthCheckResult> CheckHealthAsync(CancellationToken cancellationToken = default(CancellationToken))
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default(CancellationToken))
{
await connection.ReadEventAsync("test", 1, false);
return new HealthCheckResult(true, "Querying test event from event store.");
return HealthCheckResult.Healthy("Application must query data from EventStore.");
}
}
}

13
src/Squidex.Infrastructure.MongoDb/Diagnostics/MongoDBHealthCheck.cs

@ -5,9 +5,9 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using MongoDB.Driver;
namespace Squidex.Infrastructure.Diagnostics
@ -16,11 +16,6 @@ namespace Squidex.Infrastructure.Diagnostics
{
private readonly IMongoDatabase mongoDatabase;
public IEnumerable<string> Scopes
{
get { yield return HealthCheckScopes.Node; }
}
public MongoDBHealthCheck(IMongoDatabase mongoDatabase)
{
Guard.NotNull(mongoDatabase, nameof(mongoDatabase));
@ -28,13 +23,15 @@ namespace Squidex.Infrastructure.Diagnostics
this.mongoDatabase = mongoDatabase;
}
public async Task<HealthCheckResult> CheckHealthAsync(CancellationToken cancellationToken = default(CancellationToken))
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default(CancellationToken))
{
var collectionNames = await mongoDatabase.ListCollectionNamesAsync(cancellationToken: cancellationToken);
var result = await collectionNames.AnyAsync(cancellationToken);
return new HealthCheckResult(result);
var status = result ? HealthStatus.Healthy : HealthStatus.Unhealthy;
return new HealthCheckResult(status, "Application must query data from MongoDB");
}
}
}

29
src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsageRepository.cs

@ -35,30 +35,35 @@ namespace Squidex.Infrastructure.UsageTracking
new CreateIndexModel<MongoUsage>(Index.Ascending(x => x.Key).Ascending(x => x.Category).Ascending(x => x.Date)), cancellationToken: ct);
}
public async Task TrackUsagesAsync(params UsageUpdate[] updates)
public async Task TrackUsagesAsync(UsageUpdate update)
{
if (updates.Length == 1)
Guard.NotNull(update, nameof(update));
if (update.Counters.Count > 0)
{
var value = updates[0];
var (filter, updateStatement) = CreateOperation(update);
if (value.Counters.Count > 0)
{
var (filter, update) = CreateOperation(value);
await Collection.UpdateOneAsync(filter, updateStatement);
}
}
await Collection.UpdateOneAsync(filter, update, Upsert);
}
public async Task TrackUsagesAsync(params UsageUpdate[] updates)
{
if (updates.Length == 1)
{
await TrackUsagesAsync(updates[0]);
}
else if (updates.Length > 0)
{
var writes = new List<WriteModel<MongoUsage>>();
foreach (var value in updates)
foreach (var update in updates)
{
if (value.Counters.Count > 0)
if (update.Counters.Count > 0)
{
var (filter, update) = CreateOperation(value);
var (filter, updateStatement) = CreateOperation(update);
writes.Add(new UpdateOneModel<MongoUsage>(filter, update) { IsUpsert = true });
writes.Add(new UpdateOneModel<MongoUsage>(filter, updateStatement) { IsUpsert = true });
}
}

16
src/Squidex.Infrastructure/Diagnostics/GCHealthCheck.cs

@ -9,6 +9,7 @@ using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Options;
namespace Squidex.Infrastructure.Diagnostics
@ -17,11 +18,6 @@ namespace Squidex.Infrastructure.Diagnostics
{
private readonly long threshold;
public IEnumerable<string> Scopes
{
get { yield return HealthCheckScopes.Node; }
}
public GCHealthCheck(IOptions<GCHealthCheckOptions> options)
{
Guard.NotNull(options, nameof(options));
@ -29,19 +25,21 @@ namespace Squidex.Infrastructure.Diagnostics
threshold = 1024 * 1024 * options.Value.Threshold;
}
public Task<HealthCheckResult> CheckHealthAsync(CancellationToken cancellationToken = default(CancellationToken))
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default(CancellationToken))
{
var allocated = GC.GetTotalMemory(false);
var data = new Dictionary<string, object>()
var data = new Dictionary<string, object>
{
{ "Allocated", allocated },
{ "Allocated", allocated.ToReadableSize() },
{ "Gen0Collections", GC.CollectionCount(0) },
{ "Gen1Collections", GC.CollectionCount(1) },
{ "Gen2Collections", GC.CollectionCount(2) },
};
return Task.FromResult(new HealthCheckResult(allocated < threshold, $"Reports degraded status if allocated bytes >= {threshold.ToReadableSize()}", data));
var status = allocated < threshold ? HealthStatus.Healthy : HealthStatus.Unhealthy;
return Task.FromResult(new HealthCheckResult(status, $"Application must consum less than {threshold.ToReadableSize()} memory.", data: data));
}
}
}

27
src/Squidex.Infrastructure/Diagnostics/HealthCheckResult.cs

@ -1,27 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
namespace Squidex.Infrastructure.Diagnostics
{
public sealed class HealthCheckResult
{
public bool IsHealthy { get; }
public string Description { get; }
public Dictionary<string, object> Data { get; }
public HealthCheckResult(bool isHealthy, string description = null, Dictionary<string, object> data = null)
{
IsHealthy = isHealthy;
Data = data;
Description = description;
}
}
}

16
src/Squidex.Infrastructure/Diagnostics/HealthCheckScopes.cs

@ -1,16 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Infrastructure.Diagnostics
{
public static class HealthCheckScopes
{
public const string Any = "*";
public const string Node = "node";
public const string Cluster = "cluster";
}
}

20
src/Squidex.Infrastructure/Diagnostics/IHealthCheck.cs

@ -1,20 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Squidex.Infrastructure.Diagnostics
{
public interface IHealthCheck
{
IEnumerable<string> Scopes { get; }
Task<HealthCheckResult> CheckHealthAsync(CancellationToken cancellationToken = default(CancellationToken));
}
}

13
src/Squidex.Infrastructure/Diagnostics/OrleansHealthCheck.cs

@ -5,9 +5,9 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Orleans;
using Orleans.Runtime;
@ -17,11 +17,6 @@ namespace Squidex.Infrastructure.Diagnostics
{
private readonly IManagementGrain managementGrain;
public IEnumerable<string> Scopes
{
get { yield return HealthCheckScopes.Cluster; }
}
public OrleansHealthCheck(IGrainFactory grainFactory)
{
Guard.NotNull(grainFactory, nameof(grainFactory));
@ -29,11 +24,13 @@ namespace Squidex.Infrastructure.Diagnostics
managementGrain = grainFactory.GetGrain<IManagementGrain>(0);
}
public async Task<HealthCheckResult> CheckHealthAsync(CancellationToken cancellationToken = default(CancellationToken))
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default(CancellationToken))
{
var activationCount = await managementGrain.GetTotalActivationCount();
return new HealthCheckResult(activationCount > 0, "Orleans must have at least one activation.");
var status = activationCount > 0 ? HealthStatus.Healthy : HealthStatus.Unhealthy;
return new HealthCheckResult(status, "Orleans must have at least one activation.");
}
}
}

10
src/Squidex.Infrastructure/HashSet.cs

@ -15,5 +15,15 @@ namespace Squidex.Infrastructure
{
return new HashSet<T>(items);
}
public static HashSet<T> Of<T>(T item1)
{
return new HashSet<T> { item1 };
}
public static HashSet<T> Of<T>(T item1, T item2)
{
return new HashSet<T> { item1, item2 };
}
}
}

9
src/Squidex.Infrastructure/Squidex.Infrastructure.csproj

@ -8,16 +8,17 @@
<DebugSymbols>True</DebugSymbols>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="2.1.2" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="2.1.1" />
<PackageReference Include="Microsoft.OData.Core" Version="7.5.1" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="2.2.0" />
<PackageReference Include="Microsoft.OData.Core" Version="7.5.2" />
<PackageReference Include="Microsoft.Orleans.CodeGenerator.MSBuild" Version="2.1.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Orleans.Core" Version="2.1.2" />
<PackageReference Include="Microsoft.Orleans.OrleansRuntime" Version="2.1.2" />
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
<PackageReference Include="NodaTime" Version="2.4.2" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="SixLabors.ImageSharp" Version="1.0.0-beta0004" />

2
src/Squidex.Infrastructure/UsageTracking/IUsageRepository.cs

@ -13,6 +13,8 @@ namespace Squidex.Infrastructure.UsageTracking
{
public interface IUsageRepository
{
Task TrackUsagesAsync(UsageUpdate update);
Task TrackUsagesAsync(params UsageUpdate[] updates);
Task<IReadOnlyList<StoredUsage>> QueryAsync(string key, DateTime fromDate, DateTime toDate);

33
src/Squidex/Areas/Api/Config/Swagger/FixProcessor.cs

@ -0,0 +1,33 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading.Tasks;
using NJsonSchema;
using NSwag.SwaggerGeneration.Processors;
using NSwag.SwaggerGeneration.Processors.Contexts;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Areas.Api.Config.Swagger
{
public class FixProcessor : IOperationProcessor
{
private static readonly JsonSchema4 StringSchema = new JsonSchema4 { Type = JsonObjectType.String };
public Task<bool> ProcessAsync(OperationProcessorContext context)
{
foreach (var parameter in context.Parameters.Values)
{
if (parameter.IsRequired && parameter.Schema != null && parameter.Schema.Type == JsonObjectType.String)
{
parameter.Schema = StringSchema;
}
}
return TaskHelper.True;
}
}
}

21
src/Squidex/Areas/Api/Config/Swagger/ODataQueryParamsProcessor.cs

@ -16,30 +16,33 @@ namespace Squidex.Areas.Api.Config.Swagger
{
public sealed class ODataQueryParamsProcessor : IOperationProcessor
{
private readonly string path;
private readonly string supportedPath;
private readonly string entity;
private readonly bool supportSearch;
public ODataQueryParamsProcessor(string path, string entity, bool supportSearch)
public ODataQueryParamsProcessor(string supportedPath, string entity, bool supportSearch)
{
this.path = path;
this.entity = entity;
this.supportSearch = supportSearch;
this.supportedPath = supportedPath;
}
public Task<bool> ProcessAsync(OperationProcessorContext context)
{
if (context.OperationDescription.Path == path)
if (context.OperationDescription.Path == supportedPath)
{
var operation = context.OperationDescription.Operation;
if (supportSearch)
{
context.OperationDescription.Operation.AddQueryParameter("$search", JsonObjectType.String, "Optional OData full text search.");
operation.AddQueryParameter("$search", JsonObjectType.String, "Optional OData full text search.");
}
context.OperationDescription.Operation.AddQueryParameter("$top", JsonObjectType.Number, $"Optional number of {entity} to take.");
context.OperationDescription.Operation.AddQueryParameter("$skip", JsonObjectType.Number, $"Optional number of {entity} to skip.");
context.OperationDescription.Operation.AddQueryParameter("$orderby", JsonObjectType.String, "Optional OData order definition.");
context.OperationDescription.Operation.AddQueryParameter("$filter", JsonObjectType.String, "Optional OData filter definition.");
operation.AddQueryParameter("$top", JsonObjectType.Number, $"Optional number of {entity} to take.");
operation.AddQueryParameter("$skip", JsonObjectType.Number, $"Optional number of {entity} to skip.");
operation.AddQueryParameter("$orderby", JsonObjectType.String, "Optional OData order definition.");
operation.AddQueryParameter("$filter", JsonObjectType.String, "Optional OData filter definition.");
}
return TaskHelper.True;

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

@ -15,10 +15,11 @@ 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
{
public class ScopesProcessor : IOperationProcessor
public sealed class ScopesProcessor : IOperationProcessor
{
public Task<bool> ProcessAsync(OperationProcessorContext context)
{
@ -27,19 +28,32 @@ namespace Squidex.Areas.Api.Config.Swagger
context.OperationDescription.Operation.Security = new List<SwaggerSecurityRequirement>();
}
var authorizeAttributes =
context.MethodInfo.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Union(
context.MethodInfo.DeclaringType.GetTypeInfo().GetCustomAttributes(true).OfType<AuthorizeAttribute>()).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;
}

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

@ -0,0 +1,48 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using Microsoft.Extensions.Options;
using NSwag;
using NSwag.SwaggerGeneration.Processors.Security;
using Squidex.Config;
using Squidex.Pipeline.Swagger;
namespace Squidex.Areas.Api.Config.Swagger
{
public class SecurityProcessor : SecurityDefinitionAppender
{
public SecurityProcessor(IOptions<MyUrlsOptions> urlOptions)
: base(Constants.SecurityDefinition, CreateOAuthSchema(urlOptions.Value))
{
}
private static SwaggerSecurityScheme CreateOAuthSchema(MyUrlsOptions urlOptions)
{
var securityScheme = new SwaggerSecurityScheme();
var tokenUrl = urlOptions.BuildUrl($"{Constants.IdentityServerPrefix}/connect/token", false);
securityScheme.TokenUrl = tokenUrl;
var securityDocs = NSwagHelper.LoadDocs("security");
var securityText = securityDocs.Replace("<TOKEN_URL>", tokenUrl);
securityScheme.Description = securityText;
securityScheme.Type = SwaggerSecuritySchemeType.OAuth2;
securityScheme.Flow = SwaggerOAuth2Flow.Application;
securityScheme.Scopes = new Dictionary<string, string>
{
[Constants.ApiScope] = "Read and write access to the API"
};
return securityScheme;
}
}
}

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

@ -6,10 +6,6 @@
// ==========================================================================
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using NSwag.AspNetCore;
using Squidex.Config;
namespace Squidex.Areas.Api.Config.Swagger
{
@ -17,16 +13,7 @@ namespace Squidex.Areas.Api.Config.Swagger
{
public static void UseMySwagger(this IApplicationBuilder app)
{
var urlOptions = app.ApplicationServices.GetRequiredService<IOptions<MyUrlsOptions>>().Value;
app.UseSwaggerWithApiExplorer(settings =>
{
settings.AddAssetODataParams();
settings.ConfigureNames();
settings.ConfigurePaths(urlOptions);
settings.ConfigureSchemaSettings();
settings.ConfigureIdentity(urlOptions);
});
app.UseSwagger();
}
}
}

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

@ -7,18 +7,15 @@
using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using NJsonSchema;
using NJsonSchema.Generation.TypeMappers;
using NodaTime;
using NSwag.AspNetCore;
using NSwag.SwaggerGeneration;
using NSwag.SwaggerGeneration.Processors.Security;
using NSwag.SwaggerGeneration.Processors;
using Squidex.Areas.Api.Controllers.Contents.Generator;
using Squidex.Areas.Api.Controllers.Rules.Models;
using Squidex.Config;
using Squidex.Infrastructure;
using Squidex.Pipeline.Swagger;
namespace Squidex.Areas.Api.Config.Swagger
{
@ -26,90 +23,59 @@ namespace Squidex.Areas.Api.Config.Swagger
{
public static void AddMySwaggerSettings(this IServiceCollection services)
{
services.AddSingleton(typeof(SwaggerSettings<SwaggerGeneratorSettings>), s =>
{
var urlOptions = s.GetRequiredService<IOptions<MyUrlsOptions>>().Value;
services.AddSingletonAs<RuleActionProcessor>()
.As<IDocumentProcessor>();
var settings = new SwaggerSettings<SwaggerGeneratorSettings>()
.AddAssetODataParams()
.ConfigureNames()
.ConfigurePaths(urlOptions)
.ConfigureSchemaSettings()
.ConfigureIdentity(urlOptions);
services.AddSingletonAs<ThemeProcessor>()
.As<IDocumentProcessor>();
return settings;
});
services.AddSingletonAs<XmlTagProcessor>()
.As<IDocumentProcessor>();
services.AddTransient<SchemasSwaggerGenerator>();
}
services.AddSingletonAs<SecurityProcessor>()
.As<IDocumentProcessor>();
public static SwaggerSettings<T> ConfigureNames<T>(this SwaggerSettings<T> settings) where T : SwaggerGeneratorSettings, new()
{
settings.GeneratorSettings.Title = "Squidex API";
settings.GeneratorSettings.Version = "1.0";
services.AddSingletonAs<ScopesProcessor>()
.As<IOperationProcessor>();
return settings;
}
services.AddSingletonAs<FixProcessor>()
.As<IOperationProcessor>();
public static SwaggerSettings<T> AddAssetODataParams<T>(this SwaggerSettings<T> settings) where T : SwaggerGeneratorSettings, new()
{
settings.GeneratorSettings.OperationProcessors.Add(new ODataQueryParamsProcessor("/apps/{app}/assets", "assets", false));
services.AddSingletonAs<TagByGroupNameProcessor>()
.As<IOperationProcessor>();
return settings;
}
services.AddSingletonAs<XmlResponseTypesProcessor>()
.As<IOperationProcessor>();
public static SwaggerSettings<T> ConfigureIdentity<T>(this SwaggerSettings<T> settings, MyUrlsOptions urlOptions) where T : SwaggerGeneratorSettings, new()
{
settings.GeneratorSettings.DocumentProcessors.Add(
new SecurityDefinitionAppender(
Constants.SecurityDefinition, SwaggerHelper.CreateOAuthSchema(urlOptions)));
services.AddOpenApiDocument(settings =>
{
settings.ConfigureName();
settings.ConfigureSchemaSettings();
settings.GeneratorSettings.OperationProcessors.Add(new ScopesProcessor());
settings.OperationProcessors.Add(new ODataQueryParamsProcessor("/apps/{app}/assets", "assets", false));
});
return settings;
services.AddTransient<SchemasSwaggerGenerator>();
}
public static SwaggerSettings<T> ConfigurePaths<T>(this SwaggerSettings<T> settings, MyUrlsOptions urlOptions) where T : SwaggerGeneratorSettings, new()
public static void ConfigureName<T>(this T settings) where T : SwaggerGeneratorSettings
{
settings.SwaggerRoute = $"{Constants.ApiPrefix}/swagger/v1/swagger.json";
settings.PostProcess = document =>
{
document.BasePath = Constants.ApiPrefix;
document.Info.ExtensionData = new Dictionary<string, object>
{
["x-logo"] = new { url = urlOptions.BuildUrl("images/logo-white.png", false), backgroundColor = "#3f83df" }
};
};
settings.MiddlewareBasePath = Constants.ApiPrefix;
return settings;
settings.Title = "Squidex API";
}
public static SwaggerSettings<T> ConfigureSchemaSettings<T>(this SwaggerSettings<T> settings) where T : SwaggerGeneratorSettings, new()
public static void ConfigureSchemaSettings<T>(this T settings) where T : SwaggerGeneratorSettings
{
settings.GeneratorSettings.DefaultEnumHandling = EnumHandling.String;
settings.GeneratorSettings.DefaultPropertyNameHandling = PropertyNameHandling.CamelCase;
settings.GeneratorSettings.TypeMappers = new List<ITypeMapper>
settings.TypeMappers = new List<ITypeMapper>
{
new PrimitiveTypeMapper(typeof(Instant), schema =>
{
schema.Type = JsonObjectType.String;
schema.Format = JsonFormatStrings.DateTime;
}),
new PrimitiveTypeMapper(typeof(Language), s => s.Type = JsonObjectType.String),
new PrimitiveTypeMapper(typeof(RefToken), s => s.Type = JsonObjectType.String)
};
settings.GeneratorSettings.DocumentProcessors.Add(new RuleActionProcessor());
settings.GeneratorSettings.DocumentProcessors.Add(new XmlTagProcessor());
settings.GeneratorSettings.OperationProcessors.Add(new TagByGroupNameProcessor());
settings.GeneratorSettings.OperationProcessors.Add(new XmlResponseTypesProcessor());
return settings;
}
}
}

8
src/Squidex/Areas/Api/Config/Swagger/TagByGroupNameProcessor.cs

@ -24,9 +24,13 @@ namespace Squidex.Areas.Api.Config.Swagger
if (!string.IsNullOrWhiteSpace(groupName))
{
context.OperationDescription.Operation.Tags = new List<string> { groupName };
}
return TaskHelper.True;
return TaskHelper.True;
}
else
{
return TaskHelper.False;
}
}
}
}

41
src/Squidex/Areas/Api/Config/Swagger/ThemeProcessor.cs

@ -0,0 +1,41 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using NSwag.SwaggerGeneration.Processors;
using NSwag.SwaggerGeneration.Processors.Contexts;
using Squidex.Config;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Areas.Api.Config.Swagger
{
public sealed class ThemeProcessor : IDocumentProcessor
{
private const string Background = "#3f83df";
private readonly string url;
public ThemeProcessor(IOptions<MyUrlsOptions> urlOptions)
{
url = urlOptions.Value.BuildUrl("images/logo-white.png", false);
}
public Task ProcessAsync(DocumentProcessorContext context)
{
context.Document.BasePath = Constants.ApiPrefix;
context.Document.Info.ExtensionData = new Dictionary<string, object>
{
["x-logo"] = new { url, backgroundColor = Background }
};
return TaskHelper.Done;
}
}
}

9
src/Squidex/Areas/Api/Config/Swagger/XmlResponseTypesProcessor.cs

@ -60,18 +60,15 @@ namespace Squidex.Areas.Api.Config.Swagger
private static async Task AddInternalErrorResponseAsync(OperationProcessorContext context, SwaggerOperation operation)
{
if (operation.Responses.ContainsKey("500"))
if (!operation.Responses.ContainsKey("500"))
{
return;
operation.AddResponse("500", "Operation failed", await context.SchemaGenerator.GetErrorDtoSchemaAsync(context.SchemaResolver));
}
operation.AddResponse("500", "Operation failed", await context.SchemaGenerator.GetErrorDtoSchemaAsync(context.SchemaResolver));
}
private static void RemoveOkResponse(SwaggerOperation operation)
{
if (operation.Responses.TryGetValue("200", out var response) &&
response.Description?.Contains("=>") == true)
if (operation.Responses.TryGetValue("200", out var response) && response.Description?.Contains("=>") == true)
{
operation.Responses.Remove("200");
}

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

@ -8,8 +8,8 @@
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using NJsonSchema.Infrastructure;
using NSwag.Annotations;
using NSwag.SwaggerGeneration.Processors;
using NSwag.SwaggerGeneration.Processors.Contexts;
using Squidex.Infrastructure.Tasks;
@ -22,11 +22,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)]

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

@ -47,7 +47,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
{
var schemas = await appProvider.GetSchemasAsync(AppId);
var swaggerDocument = await schemasSwaggerGenerator.Generate(App, schemas);
var swaggerDocument = await schemasSwaggerGenerator.Generate(HttpContext, App, schemas);
return Content(swaggerDocument.ToJson(), "application/json");
}

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

@ -25,6 +25,7 @@ using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Contents
{
[ApiExplorerSettings(GroupName = nameof(Contents))]
public sealed class ContentsController : ApiController
{
private readonly IOptions<MyContentsControllerOptions> controllerOptions;
@ -75,7 +76,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>
@ -180,7 +181,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
}
/// <summary>
/// Get a content item with a specific version.
/// Get a content by version.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param>
@ -435,7 +436,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
}
/// <summary>
/// Discard changes of a content item.
/// Discard changes.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</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 }
});
}
}
}

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

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
@ -14,6 +15,9 @@ using NJsonSchema;
using NSwag;
using NSwag.AspNetCore;
using NSwag.SwaggerGeneration;
using NSwag.SwaggerGeneration.Processors;
using NSwag.SwaggerGeneration.Processors.Contexts;
using Squidex.Areas.Api.Config.Swagger;
using Squidex.Config;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Schemas;
@ -24,31 +28,48 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
{
public sealed class SchemasSwaggerGenerator
{
private readonly HttpContext context;
private readonly SwaggerSettings<SwaggerGeneratorSettings> settings;
private readonly MyUrlsOptions urlOptions;
private readonly SwaggerDocumentSettings settings = new SwaggerDocumentSettings();
private SwaggerJsonSchemaGenerator schemaGenerator;
private JsonSchemaResolver schemaResolver;
private SwaggerDocument document;
private JsonSchemaResolver schemaResolver;
public SchemasSwaggerGenerator(IHttpContextAccessor context, SwaggerSettings<SwaggerGeneratorSettings> settings, IOptions<MyUrlsOptions> urlOptions)
public SchemasSwaggerGenerator(IOptions<MyUrlsOptions> urlOptions, IEnumerable<IDocumentProcessor> documentProcessors)
{
this.context = context.HttpContext;
this.settings = settings;
this.urlOptions = urlOptions.Value;
settings.ConfigureSchemaSettings();
foreach (var processor in documentProcessors)
{
settings.DocumentProcessors.Add(processor);
}
}
public async Task<SwaggerDocument> Generate(IAppEntity app, IEnumerable<ISchemaEntity> schemas)
public async Task<SwaggerDocument> Generate(HttpContext httpContext, IAppEntity app, IEnumerable<ISchemaEntity> schemas)
{
document = SwaggerHelper.CreateApiDocument(context, urlOptions, app.Name);
document = NSwagHelper.CreateApiDocument(httpContext, urlOptions, app.Name);
schemaGenerator = new SwaggerJsonSchemaGenerator(settings.GeneratorSettings);
schemaResolver = new SwaggerSchemaResolver(document, settings.GeneratorSettings);
schemaGenerator = new SwaggerJsonSchemaGenerator(settings);
schemaResolver = new SwaggerSchemaResolver(document, settings);
GenerateSchemasOperations(schemas, app);
await GenerateDefaultErrorsAsync();
var context =
new DocumentProcessorContext(document,
Enumerable.Empty<Type>(),
Enumerable.Empty<Type>(),
schemaResolver,
schemaGenerator,
settings);
foreach (var processor in settings.DocumentProcessors)
{
await processor.ProcessAsync(context);
}
return document;
}
@ -58,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();
}
}

54
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,37 +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>

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

@ -6,10 +6,10 @@
// ==========================================================================
using System.Threading.Tasks;
using CoreTweet;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Squidex.Extensions.Actions.Twitter;
using static CoreTweet.OAuth;
namespace Squidex.Areas.Api.Controllers.Rules
{
@ -35,7 +35,7 @@ namespace Squidex.Areas.Api.Controllers.Rules
[Route("rules/twitter/auth")]
public async Task<IActionResult> Auth()
{
var session = await AuthorizeAsync(twitterOptions.ClientId, twitterOptions.ClientSecret);
var session = await OAuth.AuthorizeAsync(twitterOptions.ClientId, twitterOptions.ClientSecret);
return Ok(new
{
@ -49,7 +49,7 @@ namespace Squidex.Areas.Api.Controllers.Rules
[Route("rules/twitter/token")]
public async Task<IActionResult> AuthComplete([FromBody] TokenRequest request)
{
var session = new OAuthSession
var session = new OAuth.OAuthSession
{
ConsumerKey = twitterOptions.ClientId,
ConsumerSecret = twitterOptions.ClientSecret,

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>

5
src/Squidex/Config/Domain/EntitiesServices.cs

@ -20,7 +20,6 @@ using Squidex.Domain.Apps.Core.Tags;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Domain.Apps.Entities.Apps.Diagnostics;
using Squidex.Domain.Apps.Entities.Apps.Indexes;
using Squidex.Domain.Apps.Entities.Apps.Templates;
using Squidex.Domain.Apps.Entities.Assets;
@ -42,7 +41,6 @@ using Squidex.Domain.Apps.Entities.Schemas.Indexes;
using Squidex.Domain.Apps.Entities.Tags;
using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Diagnostics;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Migrations;
using Squidex.Pipeline;
@ -97,9 +95,6 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<SchemaHistoryEventsCreator>()
.As<IHistoryEventsCreator>();
services.AddSingletonAs<OrleansAppsHealthCheck>()
.As<IHealthCheck>();
services.AddSingletonAs<RolePermissionsProvider>()
.AsSelf();

7
src/Squidex/Config/Domain/EventStoreServices.cs

@ -48,11 +48,14 @@ namespace Squidex.Config.Domain
var connection = EventStoreConnection.Create(eventStoreConfiguration);
services.AddSingletonAs(c => new GetEventStoreHealthCheck(connection))
.As<IHealthCheck>();
services.AddSingletonAs(connection)
.As<IEventStoreConnection>();
services.AddSingletonAs(c => new GetEventStore(connection, c.GetRequiredService<IJsonSerializer>(), eventStorePrefix, eventStoreProjectionHost))
.As<IEventStore>();
services.AddHealthChecks()
.AddCheck<GetEventStoreHealthCheck>("EventStore", tags: new[] { "node" });
}
});

12
src/Squidex/Config/Domain/InfrastructureServices.cs

@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using NodaTime;
using Squidex.Domain.Apps.Entities.Apps.Diagnostics;
using Squidex.Domain.Users;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
@ -27,6 +28,11 @@ namespace Squidex.Config.Domain
{
public static void AddMyInfrastructureServices(this IServiceCollection services)
{
services.AddHealthChecks()
.AddCheck<GCHealthCheck>("GC", tags: new[] { "node" })
.AddCheck<OrleansHealthCheck>("Orleans", tags: new[] { "cluster" })
.AddCheck<OrleansAppsHealthCheck>("Orleans App", tags: new[] { "cluster" });
services.AddSingletonAs(SystemClock.Instance)
.As<IClock>();
@ -39,12 +45,6 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<AsyncLocalCache>()
.As<ILocalCache>();
services.AddSingletonAs<GCHealthCheck>()
.As<IHealthCheck>();
services.AddSingletonAs<OrleansHealthCheck>()
.As<IHealthCheck>();
services.AddSingletonAs<HttpContextAccessor>()
.As<IHttpContextAccessor>();

6
src/Squidex/Config/Domain/LoggingExtensions.cs

@ -28,11 +28,13 @@ namespace Squidex.Config.Domain
var logged = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var kvp in config.AsEnumerable().Where(kvp => kvp.Value != null).Select(x => new { Key = x.Key.ToLowerInvariant(), x.Value }).OrderBy(x => x.Key))
var orderedConfigs = config.AsEnumerable().Where(kvp => kvp.Value != null).OrderBy(x => x.Key, StringComparer.OrdinalIgnoreCase);
foreach (var kvp in orderedConfigs)
{
if (logged.Add(kvp.Key))
{
c.WriteProperty(kvp.Key, kvp.Value);
c.WriteProperty(kvp.Key.ToLowerInvariant(), kvp.Value);
}
}
}));

4
src/Squidex/Config/Domain/StoreServices.cs

@ -60,8 +60,8 @@ namespace Squidex.Config.Domain
services.AddSingletonAs(mongoDatabase)
.As<IMongoDatabase>();
services.AddSingletonAs<MongoDBHealthCheck>()
.As<IHealthCheck>();
services.AddHealthChecks()
.AddCheck<MongoDBHealthCheck>("MongoDB", tags: new[] { "node" });
services.AddSingletonAs<MongoMigrationStatus>()
.As<IMigrationStatus>();

55
src/Squidex/Config/Web/WebExtensions.cs

@ -5,11 +5,18 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpOverrides;
using Squidex.Infrastructure.Diagnostics;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Squidex.Infrastructure.Json;
using Squidex.Pipeline;
using Squidex.Pipeline.Diagnostics;
using Squidex.Pipeline.Robots;
namespace Squidex.Config.Web
@ -32,19 +39,51 @@ namespace Squidex.Config.Web
public static IApplicationBuilder UseMyHealthCheck(this IApplicationBuilder app)
{
app.Map("/readiness", builder =>
var serializer = app.ApplicationServices.GetRequiredService<IJsonSerializer>();
var writer = new Func<HttpContext, HealthReport, Task>((httpContext, report) =>
{
var response = new
{
Entries = report.Entries.ToDictionary(x => x.Key, x =>
{
var value = x.Value;
return new
{
Data = value.Data.Count > 0 ? new Dictionary<string, object>(value.Data) : null,
value.Description,
value.Duration,
value.Status
};
}),
report.Status,
report.TotalDuration
};
var json = serializer.Serialize(response);
httpContext.Response.Headers["Content-Types"] = "text/json";
return httpContext.Response.WriteAsync(json);
});
app.UseHealthChecks("/readiness", new HealthCheckOptions
{
builder.UseMiddleware<HealthCheckMiddleware>(HealthCheckScopes.Any);
Predicate = check => true,
ResponseWriter = writer
});
app.Map("/healthz", builder =>
app.UseHealthChecks("/healthz", new HealthCheckOptions
{
builder.UseMiddleware<HealthCheckMiddleware>(HealthCheckScopes.Node);
Predicate = check => check.Tags.Contains("node"),
ResponseWriter = writer
});
app.Map("/cluster-healthz", builder =>
app.UseHealthChecks("/cluster-healthz", new HealthCheckOptions
{
builder.UseMiddleware<HealthCheckMiddleware>(HealthCheckScopes.Cluster);
Predicate = check => check.Tags.Contains("cluster"),
ResponseWriter = writer
});
return app;

4
src/Squidex/Config/Web/WebServices.cs

@ -9,7 +9,6 @@ using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Config.Domain;
using Squidex.Pipeline;
using Squidex.Pipeline.Diagnostics;
using Squidex.Pipeline.Robots;
namespace Squidex.Config.Web
@ -27,9 +26,6 @@ namespace Squidex.Config.Web
services.AddSingletonAs<AppResolver>()
.AsSelf();
services.AddSingletonAs<HealthCheckMiddleware>()
.AsSelf();
services.AddSingletonAs<RobotsTxtMiddleware>()
.AsSelf();

2
src/Squidex/Dockerfile

@ -1,4 +1,4 @@
FROM microsoft/dotnet:2.1.0-aspnetcore-runtime
FROM microsoft/dotnet:2.2.0-aspnetcore-runtime
WORKDIR /app

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;

113
src/Squidex/Pipeline/Diagnostics/HealthCheckMiddleware.cs

@ -1,113 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Diagnostics;
using Squidex.Infrastructure.Json;
namespace Squidex.Pipeline.Diagnostics
{
public sealed class HealthCheckMiddleware
{
private const string Suffix = "HealthCheck";
private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(2);
private readonly Dictionary<string, IHealthCheck> healthChecks;
private readonly IJsonSerializer serializer;
private readonly RequestDelegate next;
private readonly List<string> scopes;
public HealthCheckMiddleware(IEnumerable<IHealthCheck> healthChecks, IJsonSerializer serializer, RequestDelegate next, string scopes)
{
Guard.NotNull(healthChecks, nameof(healthChecks));
Guard.NotNull(serializer, nameof(serializer));
this.healthChecks = healthChecks.ToDictionary(GetName);
this.next = next;
this.serializer = serializer;
this.scopes = SplitScopes(scopes);
}
public async Task Invoke(HttpContext context)
{
if (CanServeRequest(context.Request))
{
using (var cts = new CancellationTokenSource(Timeout))
{
var matchingChecks = healthChecks.Where(x => CanUseCheck(x.Value));
var results = await Task.WhenAll(matchingChecks.Select(x => MakeHealthCheckAsync(x.Key, x.Value, cts.Token)));
context.Response.StatusCode = 200;
context.Response.Headers.Add("Content-Type", "application/json");
if (results.Any(x => !x.Result.IsHealthy))
{
context.Response.StatusCode = 503;
}
var response = results.ToDictionary(x => x.Name, x => x.Result);
var json = serializer.Serialize(new { status = response });
await context.Response.WriteAsync(json);
}
}
else
{
await next(context);
}
}
private bool CanUseCheck(IHealthCheck check)
{
return scopes.Count == 0 || check.Scopes.Intersect(scopes).Any();
}
private bool CanServeRequest(HttpRequest request)
{
return HttpMethods.IsGet(request.Method) && (request.Path == "/" || string.IsNullOrEmpty(request.Path));
}
private static List<string> SplitScopes(string scopes)
{
return scopes.Split(",").Where(x => x != "*").ToList();
}
private static string GetName(IHealthCheck check)
{
var name = check.GetType().Name.ToCamelCase();
if (name.EndsWith(Suffix, StringComparison.OrdinalIgnoreCase))
{
name = name.Substring(0, name.Length - Suffix.Length);
}
return name;
}
private async Task<(string Name, HealthCheckResult Result)> MakeHealthCheckAsync(string name, IHealthCheck check, CancellationToken ct)
{
try
{
var result = await check.CheckHealthAsync(ct);
return (name, result);
}
catch
{
return (name, new HealthCheckResult(false));
}
}
}
}

38
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"))
{
@ -43,7 +43,6 @@ namespace Squidex.Pipeline.Swagger
var document = new SwaggerDocument
{
Tags = new List<SwaggerTag>(),
Schemes = new List<SwaggerSchema>
{
scheme
@ -58,13 +57,9 @@ namespace Squidex.Pipeline.Swagger
},
Info = new SwaggerInfo
{
ExtensionData = new Dictionary<string, object>
{
["x-logo"] = new { url = urlOptions.BuildUrl("images/logo-white.png", false), backgroundColor = "#3f83df" }
},
Title = $"Squidex API for {appName} App", Version = "1.0"
Title = $"Squidex API for {appName} App"
},
BasePath = "/api"
BasePath = Constants.ApiPrefix
};
if (!string.IsNullOrWhiteSpace(context.Request.Host.Value))
@ -72,34 +67,9 @@ namespace Squidex.Pipeline.Swagger
document.Host = context.Request.Host.Value;
}
document.SecurityDefinitions.Add(Constants.SecurityDefinition, CreateOAuthSchema(urlOptions));
return document;
}
public static SwaggerSecurityScheme CreateOAuthSchema(MyUrlsOptions urlOptions)
{
var tokenUrl = urlOptions.BuildUrl($"{Constants.IdentityServerPrefix}/connect/token", false);
var securityDocs = LoadDocs("security");
var securityText = securityDocs.Replace("<TOKEN_URL>", tokenUrl);
var result =
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 = securityText
};
return result;
}
public static async Task<JsonSchema4> GetErrorDtoSchemaAsync(this JsonSchemaGenerator schemaGenerator, JsonSchemaResolver resolver)
{
var errorType = typeof(ErrorDto);

2
src/Squidex/Program.cs

@ -25,7 +25,7 @@ namespace Squidex
new WebHostBuilder()
.UseKestrel(k => { k.AddServerHeader = false; })
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseIIS()
.UseStartup<WebStartup>()
.ConfigureLogging((hostingContext, builder) =>
{

63
src/Squidex/Squidex.csproj

@ -1,12 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<MvcRazorCompileOnPublish>true</MvcRazorCompileOnPublish>
<NoWarn>$(NoWarn);CS1591;1591;1573;1572;NU1605</NoWarn>
<PackageId>Squidex</PackageId>
<AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
<PreserveCompilationContext>true</PreserveCompilationContext>
<TargetFramework>netcoreapp2.1</TargetFramework>
<RuntimeFrameworkVersion>2.1.1</RuntimeFrameworkVersion>
<RuntimeFrameworkVersion>2.2.0</RuntimeFrameworkVersion>
<TargetFramework>netcoreapp2.2</TargetFramework>
<TypeScriptToolsVersion>Latest</TypeScriptToolsVersion>
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
</PropertyGroup>
@ -17,15 +14,17 @@
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Areas\IdentityServer\Config\Cert\*.*;Docs\*.md" />
<EmbeddedResource Include="Docs\*.md" />
<EmbeddedResource Include="Areas\IdentityServer\Config\Cert\*.*" />
<EmbeddedResource Include="Areas\Api\Controllers\Users\Assets\Avatar.png" />
<EmbeddedResource Remove="Assets\**" />
<Compile Remove="Assets\**" />
<Content Remove="Assets\**" />
<Content Remove="package-lock.json" />
<None Remove="Assets\**" />
<Content Remove="package.json" />
<Content Remove="tsconfig.json" />
<Content Remove="tslint.json" />
</ItemGroup>
<ItemGroup>
@ -55,32 +54,34 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Algolia.Search" Version="5.2.0" />
<PackageReference Include="Algolia.Search" Version="5.2.1" />
<PackageReference Include="Ben.BlockingDetector" Version="0.0.3" />
<PackageReference Include="EventStore.ClientAPI.NetCore" Version="4.1.0.23" />
<PackageReference Include="IdentityServer4" Version="2.2.0" />
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="2.6.0" />
<PackageReference Include="IdentityServer4.AspNetIdentity" Version="2.1.0" />
<PackageReference Include="Microsoft.AspNetCore" Version="2.1.6" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Cookies" Version="2.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="2.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.MicrosoftAccount" Version="2.1.2" />
<PackageReference Include="Microsoft.AspNetCore.HttpOverrides" Version="2.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.1.6" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.1.3" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.ViewCompilation" Version="2.1.1" />
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.1.1" />
<PackageReference Include="IdentityServer4" Version="2.3.2" />
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="2.7.0" />
<PackageReference Include="IdentityServer4.AspNetIdentity" Version="2.3.0" />
<PackageReference Include="Microsoft.AspNetCore" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Cookies" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.MicrosoftAccount" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.HealthChecks" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Api.Analyzers" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.ViewCompilation" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="2.1.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="2.1.1" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="2.2.0" />
<PackageReference Include="Microsoft.Data.Edm" Version="5.8.4" />
<PackageReference Include="Microsoft.OData.Core" Version="7.5.1" />
<PackageReference Include="Microsoft.OData.Core" Version="7.5.2" />
<PackageReference Include="Microsoft.Orleans.Client" Version="2.1.2" />
<PackageReference Include="Microsoft.Orleans.Core" Version="2.1.2" />
<PackageReference Include="Microsoft.Orleans.Core.Abstractions" Version="2.1.2" />
<PackageReference Include="Microsoft.Orleans.OrleansRuntime" Version="2.1.2" />
<PackageReference Include="MongoDB.Driver" Version="2.7.2" />
<PackageReference Include="NJsonSchema" Version="9.12.2" />
<PackageReference Include="NSwag.AspNetCore" Version="11.20.1" />
<PackageReference Include="NJsonSchema" Version="9.13.2" />
<PackageReference Include="NSwag.AspNetCore" Version="12.0.5" />
<PackageReference Include="OpenCover" Version="4.6.519" />
<PackageReference Include="Orleans.Providers.MongoDB" Version="2.0.1" />
<PackageReference Include="OrleansDashboard" Version="2.1.3" />
@ -98,6 +99,10 @@
<ContentWithTargetPath Include="@(_DocumentationFile->'%(FullPath)')" RelativePath="%(_DocumentationFile.Identity)" TargetPath="%(_DocumentationFile.Filename)%(_DocumentationFile.Extension)" CopyToPublishDirectory="PreserveNewest" />
</ItemGroup>
</Target>
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<PropertyGroup>
<CodeAnalysisRuleSet>..\..\Squidex.ruleset</CodeAnalysisRuleSet>
@ -106,4 +111,8 @@
<ItemGroup>
<AdditionalFiles Include="..\..\stylecop.json" Link="stylecop.json" />
</ItemGroup>
<PropertyGroup>
<NoWarn>$(NoWarn);CS1591;1591;1573;1572;NU1605</NoWarn>
</PropertyGroup>
</Project>

9
src/Squidex/web.config

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.webServer>
<handlers>
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified" />
</handlers>
<aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="true" stdoutLogFile=".\logs\stdout" />
</system.webServer>
</configuration>

4
tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<TargetFramework>netcoreapp2.2</TargetFramework>
<RuntimeFrameworkVersion>2.1.1</RuntimeFrameworkVersion>
<RootNamespace>Squidex.Domain.Apps.Core</RootNamespace>
</PropertyGroup>
@ -12,7 +12,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="FakeItEasy" Version="4.9.1" />
<PackageReference Include="FluentAssertions" Version="5.5.0" />
<PackageReference Include="FluentAssertions" Version="5.5.3" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" PrivateAssets="all" />

4
tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<TargetFramework>netcoreapp2.2</TargetFramework>
<RuntimeFrameworkVersion>2.1.1</RuntimeFrameworkVersion>
<RootNamespace>Squidex.Domain.Apps.Entities</RootNamespace>
</PropertyGroup>
@ -17,7 +17,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="FakeItEasy" Version="4.9.1" />
<PackageReference Include="FluentAssertions" Version="5.5.0" />
<PackageReference Include="FluentAssertions" Version="5.5.3" />
<PackageReference Include="GraphQL" Version="2.4.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
<PackageReference Include="MongoDB.Driver" Version="2.7.2" />

4
tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<TargetFramework>netcoreapp2.2</TargetFramework>
<RuntimeFrameworkVersion>2.1.1</RuntimeFrameworkVersion>
<RootNamespace>Squidex.Domain.Users</RootNamespace>
</PropertyGroup>
@ -13,7 +13,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="FakeItEasy" Version="4.9.1" />
<PackageReference Include="FluentAssertions" Version="5.5.0" />
<PackageReference Include="FluentAssertions" Version="5.5.3" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" PrivateAssets="all" />

8
tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<TargetFramework>netcoreapp2.2</TargetFramework>
<RuntimeFrameworkVersion>2.1.1</RuntimeFrameworkVersion>
<RootNamespace>Squidex.Infrastructure</RootNamespace>
</PropertyGroup>
@ -13,10 +13,10 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="FakeItEasy" Version="4.9.1" />
<PackageReference Include="FluentAssertions" Version="5.5.0" />
<PackageReference Include="FluentAssertions" Version="5.5.3" />
<PackageReference Include="Google.Cloud.Storage.V1" Version="2.2.1" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="2.1.2" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="2.1.2" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="2.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" PrivateAssets="all" />

8
tests/Squidex.Tests/Squidex.Tests.csproj

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<TargetFramework>netcoreapp2.2</TargetFramework>
<RuntimeFrameworkVersion>2.1.1</RuntimeFrameworkVersion>
<RootNamespace>Squidex</RootNamespace>
<NoWarn>$(NoWarn);NU1605</NoWarn>
@ -12,10 +12,10 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="FakeItEasy" Version="4.9.1" />
<PackageReference Include="IdentityServer4" Version="2.2.0" />
<PackageReference Include="IdentityServer4.AspNetIdentity" Version="2.1.0" />
<PackageReference Include="IdentityServer4" Version="2.3.2" />
<PackageReference Include="IdentityServer4.AspNetIdentity" Version="2.3.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
<PackageReference Include="NJsonSchema" Version="9.12.2" />
<PackageReference Include="NJsonSchema" Version="9.13.2" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" PrivateAssets="all" />
<PackageReference Include="xunit" Version="2.4.1" />

2
tools/GenerateLanguages/GenerateLanguages.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<TargetFramework>netcoreapp2.2</TargetFramework>
<OutputType>Exe</OutputType>
</PropertyGroup>
<PropertyGroup>

2
tools/Migrate_00/Migrate_00.csproj

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<TargetFramework>netcoreapp2.2</TargetFramework>
<RuntimeFrameworkVersion>2.1.1</RuntimeFrameworkVersion>
</PropertyGroup>
<ItemGroup>

Loading…
Cancel
Save