Browse Source

Net7 (#939)

* Started with main project.

* Temporary.

* Simplified type registry.

* More tests and thread safety.

* Update dockerfile and OpenApi.

* Add missing class.

* Update dependencies and OpenAPI

* Fix naming in tests

* Fix Squid/SquidMiddleware.cs

* OpenAPI fixes.

* Remove useless locks.

* Fix serializer.

* Usings simplified.

* Fix type names.

* Changelog for 7.3.

* Fix tests

* Fix tests

* Another fix for tests
pull/942/head
Sebastian Stehle 4 years ago
committed by GitHub
parent
commit
3d3391a783
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .github/workflows/check-updates.yml
  2. 16
      .github/workflows/dev.yml
  3. 12
      .github/workflows/release.yml
  4. 9
      CHANGELOG.md
  5. 4
      Dockerfile
  6. 5
      backend/.editorconfig
  7. 28
      backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj
  8. 2
      backend/i18n/translator/Squidex.Translator/Squidex.Translator.csproj
  9. 4
      backend/src/Migrations/Migrations.csproj
  10. 18
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldTypeProvider.cs
  11. 8
      backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj
  12. 16
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs
  13. 31
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTypeProvider.cs
  14. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Internal/Parser.cs
  15. 12
      backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj
  16. 6
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj
  17. 4
      backend/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs
  18. 6
      backend/src/Squidex.Domain.Apps.Entities/Apps/AppPermanentDeleter.cs
  19. 4
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetHistoryEventsCreator.cs
  20. 4
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetPermanentDeleter.cs
  21. 2
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker_EventHandling.cs
  22. 4
      backend/src/Squidex.Domain.Apps.Entities/Assets/RecursiveDeleter.cs
  23. 4
      backend/src/Squidex.Domain.Apps.Entities/Contents/ContentHistoryEventsCreator.cs
  24. 4
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs
  25. 14
      backend/src/Squidex.Domain.Apps.Entities/History/HistoryEventsCreatorBase.cs
  26. 2
      backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs
  27. 4
      backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaHistoryEventsCreator.cs
  28. 12
      backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj
  29. 4
      backend/src/Squidex.Domain.Apps.Entities/Teams/TeamHistoryEventsCreator.cs
  30. 4
      backend/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj
  31. 12
      backend/src/Squidex.Domain.Users.MongoDb/MongoRoleStore.cs
  32. 14
      backend/src/Squidex.Domain.Users.MongoDb/MongoUser.cs
  33. 51
      backend/src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs
  34. 6
      backend/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj
  35. 6
      backend/src/Squidex.Domain.Users/DefaultUserService.cs
  36. 6
      backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj
  37. 14
      backend/src/Squidex.Domain.Users/UserClaimsPrincipalFactoryWithEmail.cs
  38. 2
      backend/src/Squidex.Domain.Users/UserWithClaims.cs
  39. 2
      backend/src/Squidex.Infrastructure.Azure/Squidex.Infrastructure.Azure.csproj
  40. 6
      backend/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj
  41. 10
      backend/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj
  42. 4
      backend/src/Squidex.Infrastructure.RabbitMq/Squidex.Infrastructure.RabbitMq.csproj
  43. 2
      backend/src/Squidex.Infrastructure.Redis/Squidex.Infrastructure.Redis.csproj
  44. 29
      backend/src/Squidex.Infrastructure/EventSourcing/DefaultEventFormatter.cs
  45. 14
      backend/src/Squidex.Infrastructure/EventSourcing/EventTypeAttribute.cs
  46. 19
      backend/src/Squidex.Infrastructure/GravatarHelper.cs
  47. 13
      backend/src/Squidex.Infrastructure/Json/System/Constants.cs
  48. 52
      backend/src/Squidex.Infrastructure/Json/System/InheritanceConverter.cs
  49. 94
      backend/src/Squidex.Infrastructure/Json/System/InheritanceConverterBase.cs
  50. 104
      backend/src/Squidex.Infrastructure/Json/System/PolymorphicConverter.cs
  51. 51
      backend/src/Squidex.Infrastructure/Json/System/PolymorphicTypeResolver.cs
  52. 5
      backend/src/Squidex.Infrastructure/Json/System/ReflectionHelper.cs
  53. 33
      backend/src/Squidex.Infrastructure/Json/System/StringConverter.cs
  54. 17
      backend/src/Squidex.Infrastructure/NamedId{T}.cs
  55. 9
      backend/src/Squidex.Infrastructure/RandomHash.cs
  56. 53
      backend/src/Squidex.Infrastructure/Reflection/AssemblyTypeProvider.cs
  57. 2
      backend/src/Squidex.Infrastructure/Reflection/ITypeProvider.cs
  58. 13
      backend/src/Squidex.Infrastructure/Reflection/IgnoreEqualsAttribute.cs
  59. 83
      backend/src/Squidex.Infrastructure/Reflection/TypeConfig.cs
  60. 1
      backend/src/Squidex.Infrastructure/Reflection/TypeNameBuilder.cs
  61. 24
      backend/src/Squidex.Infrastructure/Reflection/TypeNameNotFoundException.cs
  62. 156
      backend/src/Squidex.Infrastructure/Reflection/TypeNameRegistry.cs
  63. 111
      backend/src/Squidex.Infrastructure/Reflection/TypeRegistry.cs
  64. 34
      backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj
  65. 4
      backend/src/Squidex.Infrastructure/UsageTracking/CachingUsageTracker.cs
  66. 4
      backend/src/Squidex.Shared/Squidex.Shared.csproj
  67. 9
      backend/src/Squidex.Web/Extensions.cs
  68. 108
      backend/src/Squidex.Web/Json/JsonInheritanceConverter.cs
  69. 21
      backend/src/Squidex.Web/Json/JsonInheritanceConverterAttribute.cs
  70. 4
      backend/src/Squidex.Web/Squidex.Web.csproj
  71. 15
      backend/src/Squidex/Areas/Api/Config/OpenApi/CommonProcessor.cs
  72. 67
      backend/src/Squidex/Areas/Api/Config/OpenApi/DiscriminatorProcessor.cs
  73. 69
      backend/src/Squidex/Areas/Api/Config/OpenApi/ErrorDtoProcessor.cs
  74. 30
      backend/src/Squidex/Areas/Api/Config/OpenApi/FixProcessor.cs
  75. 77
      backend/src/Squidex/Areas/Api/Config/OpenApi/OpenApiServices.cs
  76. 30
      backend/src/Squidex/Areas/Api/Config/OpenApi/SchemaNameGenerator.cs
  77. 41
      backend/src/Squidex/Areas/Api/Config/OpenApi/SecurityProcessor.cs
  78. 2
      backend/src/Squidex/Areas/Api/Config/OpenApi/TagXmlProcessor.cs
  79. 54
      backend/src/Squidex/Areas/Api/Config/OpenApi/XmlResponseTypesProcessor.cs
  80. 14
      backend/src/Squidex/Areas/Api/Controllers/Apps/AppAssetsController.cs
  81. 28
      backend/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs
  82. 26
      backend/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs
  83. 6
      backend/src/Squidex/Areas/Api/Controllers/Apps/AppImageController.cs
  84. 30
      backend/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs
  85. 36
      backend/src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs
  86. 14
      backend/src/Squidex/Areas/Api/Controllers/Apps/AppSettingsController.cs
  87. 28
      backend/src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs
  88. 59
      backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs
  89. 12
      backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs
  90. 37
      backend/src/Squidex/Areas/Api/Controllers/Assets/AssetFoldersController.cs
  91. 108
      backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs
  92. 2
      backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetContentQueryDto.cs
  93. 12
      backend/src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs
  94. 24
      backend/src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs
  95. 9
      backend/src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs
  96. 36
      backend/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs
  97. 11
      backend/src/Squidex/Areas/Api/Controllers/Comments/Notifications/UserNotificationsController.cs
  98. 130
      backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs
  99. 26
      backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsSharedController.cs
  100. 14
      backend/src/Squidex/Areas/Api/Controllers/Diagnostics/DiagnosticsController.cs

2
.github/workflows/check-updates.yml

@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2.5.0 - uses: actions/checkout@v3
with: with:
token: ${{ secrets.WORKFLOW_SECRET }} token: ${{ secrets.WORKFLOW_SECRET }}

16
.github/workflows/dev.yml

@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2.5.0 uses: actions/checkout@v3
- name: Inject slug/short variables - name: Inject slug/short variables
uses: rlespinasse/github-slug-action@v4.3.2 uses: rlespinasse/github-slug-action@v4.3.2
@ -25,7 +25,7 @@ jobs:
env: env:
BUILD_NUMBER: ${{ github.run_number }} BUILD_NUMBER: ${{ github.run_number }}
run: | run: |
echo "BUILD_NUMBER=$(($BUILD_NUMBER + 5967))" >> $GITHUB_ENV echo "BUILD_NUMBER=$(($BUILD_NUMBER + 6000))" >> $GITHUB_ENV
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v2.1.0 uses: docker/setup-qemu-action@v2.1.0
@ -73,10 +73,10 @@ jobs:
env: env:
BUILD_NUMBER: ${{ github.run_number }} BUILD_NUMBER: ${{ github.run_number }}
run: | run: |
echo "BUILD_NUMBER=$(($BUILD_NUMBER + 5967))" >> $GITHUB_ENV echo "BUILD_NUMBER=$(($BUILD_NUMBER + 6000))" >> $GITHUB_ENV
- name: Checkout - name: Checkout
uses: actions/checkout@v2.5.0 uses: actions/checkout@v3
- name: Inject slug/short variables - name: Inject slug/short variables
uses: rlespinasse/github-slug-action@v4.3.2 uses: rlespinasse/github-slug-action@v4.3.2
@ -112,7 +112,7 @@ jobs:
- name: RUN TEST - name: RUN TEST
uses: kohlerdominik/docker-run-action@v1.1.0 uses: kohlerdominik/docker-run-action@v1.1.0
with: with:
image: squidex/build image: squidex/build:7
environment: | environment: |
CONFIG__BACKUPURL=http://localhost:5000 CONFIG__BACKUPURL=http://localhost:5000
CONFIG__WAIT=60 CONFIG__WAIT=60
@ -126,7 +126,7 @@ jobs:
- name: RUN TEST on path - name: RUN TEST on path
uses: kohlerdominik/docker-run-action@v1.1.0 uses: kohlerdominik/docker-run-action@v1.1.0
with: with:
image: squidex/build image: squidex/build:7
environment: | environment: |
CONFIG__BACKUPURL=http://localhost:5000 CONFIG__BACKUPURL=http://localhost:5000
CONFIG__WAIT=60 CONFIG__WAIT=60
@ -140,7 +140,7 @@ jobs:
- name: RUN TEST with dedicated collections - name: RUN TEST with dedicated collections
uses: kohlerdominik/docker-run-action@v1.1.0 uses: kohlerdominik/docker-run-action@v1.1.0
with: with:
image: squidex/build image: squidex/build:7
environment: | environment: |
CONFIG__BACKUPURL=http://localhost:5000 CONFIG__BACKUPURL=http://localhost:5000
CONFIG__WAIT=60 CONFIG__WAIT=60
@ -171,7 +171,7 @@ jobs:
env: env:
BUILD_NUMBER: ${{ github.run_number }} BUILD_NUMBER: ${{ github.run_number }}
run: | run: |
echo "BUILD_NUMBER=$(($BUILD_NUMBER + 5967))" >> $GITHUB_ENV echo "BUILD_NUMBER=$(($BUILD_NUMBER + 6000))" >> $GITHUB_ENV
- name: Inject slug/short variables - name: Inject slug/short variables
uses: rlespinasse/github-slug-action@v4.3.2 uses: rlespinasse/github-slug-action@v4.3.2

12
.github/workflows/release.yml

@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2.5.0 uses: actions/checkout@v3
- name: Inject slug/short variables - name: Inject slug/short variables
uses: rlespinasse/github-slug-action@v4.3.2 uses: rlespinasse/github-slug-action@v4.3.2
@ -59,7 +59,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2.5.0 uses: actions/checkout@v3
- name: Inject slug/short variables - name: Inject slug/short variables
uses: rlespinasse/github-slug-action@v4.3.2 uses: rlespinasse/github-slug-action@v4.3.2
@ -95,7 +95,7 @@ jobs:
- name: RUN TEST - name: RUN TEST
uses: kohlerdominik/docker-run-action@v1.1.0 uses: kohlerdominik/docker-run-action@v1.1.0
with: with:
image: squidex/build image: squidex/build:7
environment: | environment: |
CONFIG__BACKUPURL=http://localhost:5000 CONFIG__BACKUPURL=http://localhost:5000
CONFIG__WAIT=60 CONFIG__WAIT=60
@ -109,7 +109,7 @@ jobs:
- name: RUN TEST on path - name: RUN TEST on path
uses: kohlerdominik/docker-run-action@v1.1.0 uses: kohlerdominik/docker-run-action@v1.1.0
with: with:
image: squidex/build image: squidex/build:7
environment: | environment: |
CONFIG__BACKUPURL=http://localhost:5000 CONFIG__BACKUPURL=http://localhost:5000
CONFIG__WAIT=60 CONFIG__WAIT=60
@ -123,7 +123,7 @@ jobs:
- name: RUN TEST with dedicated collections - name: RUN TEST with dedicated collections
uses: kohlerdominik/docker-run-action@v1.1.0 uses: kohlerdominik/docker-run-action@v1.1.0
with: with:
image: squidex/build image: squidex/build:7
environment: | environment: |
CONFIG__BACKUPURL=http://localhost:5000 CONFIG__BACKUPURL=http://localhost:5000
CONFIG__WAIT=60 CONFIG__WAIT=60
@ -204,7 +204,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2.5.0 uses: actions/checkout@v3
- name: Inject slug/short variables - name: Inject slug/short variables
uses: rlespinasse/github-slug-action@v4.3.2 uses: rlespinasse/github-slug-action@v4.3.2

9
CHANGELOG.md

@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [7.3.0] - 2022-18-11
This version is all about the Update to .NET 7. If you host Squidex with Containers, e.g. with Docker Compose, Kubernetes or Managed Containers nothing changes. But if you host Squidex yourself in IIS or Linux, you have to ensure that you use the latest runtime.
### Changed
* **Runtime**: Migration to .NET 7
* **API**: New system how to manage type names, e.g. for field types, rule actions and so on, with the goal to improve serialization performance.
## [7.2.0] - 2022-11-11 ## [7.2.0] - 2022-11-11
### Fixed ### Fixed

4
Dockerfile

@ -1,7 +1,7 @@
# #
# Stage 1, Build Backend # Stage 1, Build Backend
# #
FROM mcr.microsoft.com/dotnet/sdk:6.0 as backend FROM mcr.microsoft.com/dotnet/sdk:7.0 as backend
ARG SQUIDEX__VERSION=7.0.0 ARG SQUIDEX__VERSION=7.0.0
@ -68,7 +68,7 @@ RUN cp -a build /build/
# #
# Stage 3, Build runtime # Stage 3, Build runtime
# #
FROM mcr.microsoft.com/dotnet/aspnet:6.0.10-bullseye-slim FROM mcr.microsoft.com/dotnet/aspnet:7.0-bullseye-slim
# Curl for debugging and libc-dev for protobuf # Curl for debugging and libc-dev for protobuf
RUN apt-get update \ RUN apt-get update \

5
backend/.editorconfig

@ -13,6 +13,8 @@ insert_final_newline = true
indent_style = space indent_style = space
indent_size = 4 indent_size = 4
csharp_style_namespace_declarations = file_scoped
# FAILING ANALYZERS # FAILING ANALYZERS
dotnet_diagnostic.RECS0002.severity = none dotnet_diagnostic.RECS0002.severity = none
dotnet_diagnostic.RECS0117.severity = none dotnet_diagnostic.RECS0117.severity = none
@ -166,5 +168,8 @@ dotnet_diagnostic.SA1601.severity = none
# SA1602: Enumeration items should be documented # SA1602: Enumeration items should be documented
dotnet_diagnostic.SA1602.severity = none dotnet_diagnostic.SA1602.severity = none
# SA1615: Element return value should be documented
dotnet_diagnostic.SA1615.severity = none
# SA1623: Property summary documentation should match accessors # SA1623: Property summary documentation should match accessors
dotnet_diagnostic.SA1623.severity = none dotnet_diagnostic.SA1623.severity = none

28
backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
<LangVersion>10.0</LangVersion> <LangVersion>10.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup> </PropertyGroup>
@ -9,29 +9,29 @@
<ProjectReference Include="..\..\src\Squidex.Web\Squidex.Web.csproj" /> <ProjectReference Include="..\..\src\Squidex.Web\Squidex.Web.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Algolia.Search" Version="6.12.1" /> <PackageReference Include="Algolia.Search" Version="6.14.0" />
<PackageReference Include="Azure.Monitor.OpenTelemetry.Exporter" Version="1.0.0-beta.3" /> <PackageReference Include="Azure.Monitor.OpenTelemetry.Exporter" Version="1.0.0-beta.3" />
<PackageReference Include="Azure.Search.Documents" Version="11.3.0" /> <PackageReference Include="Azure.Search.Documents" Version="11.4.0" />
<PackageReference Include="Confluent.Apache.Avro" Version="1.7.7.7" /> <PackageReference Include="Confluent.Apache.Avro" Version="1.7.7.7" />
<PackageReference Include="Confluent.Kafka" Version="1.8.2" /> <PackageReference Include="Confluent.Kafka" Version="1.9.3" />
<PackageReference Include="Confluent.SchemaRegistry.Serdes" Version="1.3.0" /> <PackageReference Include="Confluent.SchemaRegistry.Serdes" Version="1.3.0" />
<PackageReference Include="CoreTweet" Version="1.0.0.483" /> <PackageReference Include="CoreTweet" Version="1.0.0.483" />
<PackageReference Include="Elasticsearch.Net" Version="7.17.4" /> <PackageReference Include="Elasticsearch.Net" Version="7.17.5" />
<PackageReference Include="Google.Cloud.Diagnostics.Common" Version="4.4.0" /> <PackageReference Include="Google.Cloud.Diagnostics.Common" Version="4.4.0" />
<PackageReference Include="Google.Cloud.Logging.V2" Version="3.6.0" /> <PackageReference Include="Google.Cloud.Logging.V2" Version="3.6.0" />
<PackageReference Include="Meziantou.Analyzer" Version="1.0.702"> <PackageReference Include="Meziantou.Analyzer" Version="1.0.746">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.Azure.CognitiveServices.Vision.ComputerVision" Version="7.0.1" /> <PackageReference Include="Microsoft.Azure.CognitiveServices.Vision.ComputerVision" Version="7.0.1" />
<PackageReference Include="Microsoft.Azure.SignalR.Management" Version="1.17.1" /> <PackageReference Include="Microsoft.Azure.SignalR.Management" Version="1.19.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="7.0.0" />
<PackageReference Include="Microsoft.OData.Core" Version="7.11.0" /> <PackageReference Include="Microsoft.OData.Core" Version="7.12.5" />
<PackageReference Include="NodaTime" Version="3.1.2" /> <PackageReference Include="NodaTime" Version="3.1.5" />
<PackageReference Include="OpenSearch.Net" Version="1.1.0" /> <PackageReference Include="OpenSearch.Net" Version="1.2.0" />
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.3.0" /> <PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.3.1" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.3.0" /> <PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.3.1" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.0.0-rc8" /> <PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.0.0-rc8" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.OpenTelemetry.Exporter.Stackdriver" Version="0.0.0-alpha.0.384" /> <PackageReference Include="Squidex.OpenTelemetry.Exporter.Stackdriver" Version="0.0.0-alpha.0.384" />

2
backend/i18n/translator/Squidex.Translator/Squidex.Translator.csproj

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
<LangVersion>10.0</LangVersion> <LangVersion>10.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<NoWarn>NU1608</NoWarn> <NoWarn>NU1608</NoWarn>

4
backend/src/Migrations/Migrations.csproj

@ -1,12 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
<LangVersion>10.0</LangVersion> <LangVersion>10.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="1.0.702"> <PackageReference Include="Meziantou.Analyzer" Version="1.0.746">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

18
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldTypeProvider.cs

@ -14,19 +14,17 @@ public class FieldTypeProvider : ITypeProvider
private const string Suffix = "Properties"; private const string Suffix = "Properties";
private const string SuffixOld = "FieldProperties"; private const string SuffixOld = "FieldProperties";
public void Map(TypeNameRegistry typeNameRegistry) public void Map(TypeRegistry typeRegistry)
{ {
var types = typeof(FieldTypeProvider).Assembly.GetTypes().Where(x => typeof(FieldProperties).IsAssignableFrom(x) && !x.IsAbstract); static IEnumerable<Type> FindTypes(Type baseType)
{
var addedTypes = new HashSet<Type>(); return baseType.Assembly.GetTypes().Where(x => baseType.IsAssignableFrom(x) && !x.IsAbstract);
}
foreach (var type in types) foreach (var type in FindTypes(typeof(FieldProperties)))
{ {
if (addedTypes.Add(type)) typeRegistry.Add<FieldProperties>(type, type.TypeName(false, SuffixOld));
{ typeRegistry.Add<FieldProperties>(type, type.TypeName(false, Suffix));
typeNameRegistry.Map(type, type.TypeName(false, Suffix));
typeNameRegistry.MapObsolete(type, type.TypeName(false, SuffixOld));
}
} }
} }
} }

8
backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
<RootNamespace>Squidex.Domain.Apps.Core</RootNamespace> <RootNamespace>Squidex.Domain.Apps.Core</RootNamespace>
<LangVersion>10.0</LangVersion> <LangVersion>10.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
@ -12,15 +12,15 @@
<DebugSymbols>True</DebugSymbols> <DebugSymbols>True</DebugSymbols>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="1.0.702"> <PackageReference Include="Meziantou.Analyzer" Version="1.0.746">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="6.0.1" /> <PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="7.0.0" />
<PackageReference Include="NetTopologySuite" Version="2.5.0" /> <PackageReference Include="NetTopologySuite" Version="2.5.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="6.0.0" /> <PackageReference Include="System.Collections.Immutable" Version="7.0.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" /> <PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

16
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs

@ -24,7 +24,7 @@ public sealed class RuleService : IRuleService
{ {
private readonly Dictionary<Type, IRuleActionHandler> ruleActionHandlers; private readonly Dictionary<Type, IRuleActionHandler> ruleActionHandlers;
private readonly Dictionary<Type, IRuleTriggerHandler> ruleTriggerHandlers; private readonly Dictionary<Type, IRuleTriggerHandler> ruleTriggerHandlers;
private readonly TypeNameRegistry typeNameRegistry; private readonly TypeRegistry typeRegistry;
private readonly RuleOptions ruleOptions; private readonly RuleOptions ruleOptions;
private readonly IEventEnricher eventEnricher; private readonly IEventEnricher eventEnricher;
private readonly IJsonSerializer serializer; private readonly IJsonSerializer serializer;
@ -39,9 +39,9 @@ public sealed class RuleService : IRuleService
IEventEnricher eventEnricher, IEventEnricher eventEnricher,
IJsonSerializer serializer, IJsonSerializer serializer,
ILogger<RuleService> log, ILogger<RuleService> log,
TypeNameRegistry typeNameRegistry) TypeRegistry typeRegistry)
{ {
this.typeNameRegistry = typeNameRegistry; this.typeRegistry = typeRegistry;
this.eventEnricher = eventEnricher; this.eventEnricher = eventEnricher;
this.ruleOptions = ruleOptions.Value; this.ruleOptions = ruleOptions.Value;
this.ruleTriggerHandlers = ruleTriggerHandlers.ToDictionary(x => x.TriggerType); this.ruleTriggerHandlers = ruleTriggerHandlers.ToDictionary(x => x.TriggerType);
@ -295,7 +295,8 @@ public sealed class RuleService : IRuleService
private async Task<JobResult> CreateJobAsync(IRuleActionHandler actionHandler, EnrichedEvent enrichedEvent, RuleContext context, Instant now) private async Task<JobResult> CreateJobAsync(IRuleActionHandler actionHandler, EnrichedEvent enrichedEvent, RuleContext context, Instant now)
{ {
var actionName = typeNameRegistry.GetName(context.Rule.Action.GetType()); var actionType = context.Rule.Action.GetType();
var actionName = typeRegistry.GetName<RuleAction>(actionType);
var expires = now.Plus(Constants.ExpirationTime); var expires = now.Plus(Constants.ExpirationTime);
@ -359,17 +360,16 @@ public sealed class RuleService : IRuleService
try try
{ {
var actionType = typeNameRegistry.GetType(actionName); var actionType = typeRegistry.GetType<RuleAction>(actionName);
var actionHandler = ruleActionHandlers[actionType]; var actionHandler = ruleActionHandlers[actionType];
var actionObject = serializer.Deserialize<object>(job, actionHandler.DataType);
var deserialized = serializer.Deserialize<object>(job, actionHandler.DataType);
using (var combined = CancellationTokenSource.CreateLinkedTokenSource(ct)) using (var combined = CancellationTokenSource.CreateLinkedTokenSource(ct))
{ {
// Enforce a timeout after a configured time span. // Enforce a timeout after a configured time span.
combined.CancelAfter(GetTimeoutInMs()); combined.CancelAfter(GetTimeoutInMs());
result = await actionHandler.ExecuteJobAsync(deserialized, combined.Token).WithCancellation(combined.Token); result = await actionHandler.ExecuteJobAsync(actionObject, combined.Token).WithCancellation(combined.Token);
} }
} }
catch (Exception ex) catch (Exception ex)

31
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTypeProvider.cs

@ -206,34 +206,17 @@ public sealed class RuleTypeProvider : ITypeProvider
return type.TypeName(false, ActionSuffix, ActionSuffixV2); return type.TypeName(false, ActionSuffix, ActionSuffixV2);
} }
public void Map(TypeNameRegistry typeNameRegistry) public void Map(TypeRegistry typeRegistry)
{ {
foreach (var (_, actionType) in actionTypes) typeRegistry.Discriminator<RuleAction>("actionType");
{
typeNameRegistry.Map(actionType.Type, actionType.Type.Name);
}
var addedTypes = new HashSet<Type>();
static IEnumerable<Type> FindTypes(Type baseType)
{
return baseType.Assembly.GetTypes().Where(x => baseType.IsAssignableFrom(x) && !x.IsAbstract);
}
foreach (var type in FindTypes(typeof(EnrichedEvent))) foreach (var (name, actionType) in actionTypes)
{ {
if (addedTypes.Add(type)) typeRegistry.Add<RuleAction>(actionType.Type, actionType.Type.Name);
{ typeRegistry.Add<RuleAction>(actionType.Type, name);
typeNameRegistry.Map(type, type.Name);
}
} }
foreach (var type in FindTypes(typeof(RuleTrigger))) typeRegistry.Map(new AssemblyTypeProvider<EnrichedEvent>());
{ typeRegistry.Map(new AssemblyTypeProvider<RuleTrigger>("triggerType"));
if (addedTypes.Add(type))
{
typeNameRegistry.Map(type, type.Name);
}
}
} }
} }

2
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Internal/Parser.cs

@ -37,6 +37,6 @@ internal sealed class Parser
var parser = new JavaScriptParser(script, DefaultParserOptions); var parser = new JavaScriptParser(script, DefaultParserOptions);
return parser.ParseScript(); return parser.ParseScript();
}); })!;
} }
} }

12
backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
<RootNamespace>Squidex.Domain.Apps.Core</RootNamespace> <RootNamespace>Squidex.Domain.Apps.Core</RootNamespace>
<LangVersion>10.0</LangVersion> <LangVersion>10.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
@ -21,16 +21,16 @@
<PackageReference Include="Fluid.Core.Squidex" Version="1.0.0-beta" /> <PackageReference Include="Fluid.Core.Squidex" Version="1.0.0-beta" />
<PackageReference Include="GeoJSON.Net" Version="1.2.19" /> <PackageReference Include="GeoJSON.Net" Version="1.2.19" />
<PackageReference Include="Jint" Version="3.0.0-beta-2036" /> <PackageReference Include="Jint" Version="3.0.0-beta-2036" />
<PackageReference Include="Meziantou.Analyzer" Version="1.0.702"> <PackageReference Include="Meziantou.Analyzer" Version="1.0.746">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
<PackageReference Include="NJsonSchema" Version="10.7.2" /> <PackageReference Include="NJsonSchema" Version="10.8.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.Messaging.Subscriptions" Version="4.13.0" /> <PackageReference Include="Squidex.Messaging.Subscriptions" Version="5.0.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="6.0.0" /> <PackageReference Include="System.Collections.Immutable" Version="7.0.0" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" /> <PackageReference Include="System.Linq.Async" Version="6.0.1" />
<PackageReference Include="ValueTaskSupplement" Version="1.1.0" /> <PackageReference Include="ValueTaskSupplement" Version="1.1.0" />
</ItemGroup> </ItemGroup>

6
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
<LangVersion>10.0</LangVersion> <LangVersion>10.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
@ -19,11 +19,11 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Lucene.Net.QueryParser" Version="4.8.0-beta00015" /> <PackageReference Include="Lucene.Net.QueryParser" Version="4.8.0-beta00015" />
<PackageReference Include="Meziantou.Analyzer" Version="1.0.702"> <PackageReference Include="Meziantou.Analyzer" Version="1.0.746">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="MongoDB.Driver" Version="2.17.1" /> <PackageReference Include="MongoDB.Driver" Version="2.18.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" /> <PackageReference Include="System.ValueTuple" Version="4.5.0" />

4
backend/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs

@ -15,8 +15,8 @@ namespace Squidex.Domain.Apps.Entities.Apps;
public sealed class AppHistoryEventsCreator : HistoryEventsCreatorBase public sealed class AppHistoryEventsCreator : HistoryEventsCreatorBase
{ {
public AppHistoryEventsCreator(TypeNameRegistry typeNameRegistry) public AppHistoryEventsCreator(TypeRegistry typeRegistry)
: base(typeNameRegistry) : base(typeRegistry)
{ {
AddEventMessage<AppCreated>( AddEventMessage<AppCreated>(
"history.apps.created"); "history.apps.created");

6
backend/src/Squidex.Domain.Apps.Entities/Apps/AppPermanentDeleter.cs

@ -30,7 +30,7 @@ public sealed class AppPermanentDeleter : IEventConsumer
get => "^app-"; get => "^app-";
} }
public AppPermanentDeleter(IEnumerable<IDeleter> deleters, IDomainObjectFactory factory, TypeNameRegistry typeNameRegistry) public AppPermanentDeleter(IEnumerable<IDeleter> deleters, IDomainObjectFactory factory, TypeRegistry typeRegistry)
{ {
this.deleters = deleters.OrderBy(x => x.Order).ToList(); this.deleters = deleters.OrderBy(x => x.Order).ToList();
this.factory = factory; this.factory = factory;
@ -38,8 +38,8 @@ public sealed class AppPermanentDeleter : IEventConsumer
// Compute the event types names once for performance reasons and use hashset for extensibility. // Compute the event types names once for performance reasons and use hashset for extensibility.
consumingTypes = new HashSet<string> consumingTypes = new HashSet<string>
{ {
typeNameRegistry.GetName<AppDeleted>(), typeRegistry.GetName<IEvent, AppDeleted>(),
typeNameRegistry.GetName<AppContributorRemoved>() typeRegistry.GetName<IEvent, AppContributorRemoved>()
}; };
} }

4
backend/src/Squidex.Domain.Apps.Entities/Assets/AssetHistoryEventsCreator.cs

@ -14,8 +14,8 @@ namespace Squidex.Domain.Apps.Entities.Assets;
public sealed class AssetHistoryEventsCreator : HistoryEventsCreatorBase public sealed class AssetHistoryEventsCreator : HistoryEventsCreatorBase
{ {
public AssetHistoryEventsCreator(TypeNameRegistry typeNameRegistry) public AssetHistoryEventsCreator(TypeRegistry typeRegistry)
: base(typeNameRegistry) : base(typeRegistry)
{ {
AddEventMessage<AssetCreated>( AddEventMessage<AssetCreated>(
"history.assets.uploaded"); "history.assets.uploaded");

4
backend/src/Squidex.Domain.Apps.Entities/Assets/AssetPermanentDeleter.cs

@ -27,14 +27,14 @@ public sealed class AssetPermanentDeleter : IEventConsumer
get => "^asset-"; get => "^asset-";
} }
public AssetPermanentDeleter(IAssetFileStore assetFileStore, TypeNameRegistry typeNameRegistry) public AssetPermanentDeleter(IAssetFileStore assetFileStore, TypeRegistry typeRegistry)
{ {
this.assetFileStore = assetFileStore; this.assetFileStore = assetFileStore;
// Compute the event types names once for performance reasons and use hashset for extensibility. // Compute the event types names once for performance reasons and use hashset for extensibility.
consumingTypes = new HashSet<string> consumingTypes = new HashSet<string>
{ {
typeNameRegistry.GetName<AssetDeleted>() typeRegistry.GetName<IEvent, AssetDeleted>()
}; };
} }

2
backend/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker_EventHandling.cs

@ -157,7 +157,7 @@ public partial class AssetUsageTracker : IEventConsumer
// Store the latest tags in memory for fast access. // Store the latest tags in memory for fast access.
if (memoryCache.TryGetValue<State>(key, out var state)) if (memoryCache.TryGetValue<State>(key, out var state))
{ {
return state.Tags; return state?.Tags;
} }
var stored = await store.ReadAsync(key, ct); var stored = await store.ReadAsync(key, ct);

4
backend/src/Squidex.Domain.Apps.Entities/Assets/RecursiveDeleter.cs

@ -37,7 +37,7 @@ public sealed class RecursiveDeleter : IEventConsumer
ICommandBus commandBus, ICommandBus commandBus,
IAssetRepository assetRepository, IAssetRepository assetRepository,
IAssetFolderRepository assetFolderRepository, IAssetFolderRepository assetFolderRepository,
TypeNameRegistry typeNameRegistry, TypeRegistry typeRegistry,
ILogger<RecursiveDeleter> log) ILogger<RecursiveDeleter> log)
{ {
this.commandBus = commandBus; this.commandBus = commandBus;
@ -49,7 +49,7 @@ public sealed class RecursiveDeleter : IEventConsumer
// Compute the event types names once for performance reasons and use hashset for extensibility. // Compute the event types names once for performance reasons and use hashset for extensibility.
consumingTypes = new HashSet<string> consumingTypes = new HashSet<string>
{ {
typeNameRegistry.GetName<AssetFolderDeleted>() typeRegistry.GetName<IEvent, AssetFolderDeleted>()
}; };
} }

4
backend/src/Squidex.Domain.Apps.Entities/Contents/ContentHistoryEventsCreator.cs

@ -15,8 +15,8 @@ namespace Squidex.Domain.Apps.Entities.Contents;
public sealed class ContentHistoryEventsCreator : HistoryEventsCreatorBase public sealed class ContentHistoryEventsCreator : HistoryEventsCreatorBase
{ {
public ContentHistoryEventsCreator(TypeNameRegistry typeNameRegistry) public ContentHistoryEventsCreator(TypeRegistry typeRegistry)
: base(typeNameRegistry) : base(typeRegistry)
{ {
AddEventMessage<ContentCreated>( AddEventMessage<ContentCreated>(
"history.contents.created"); "history.contents.created");

4
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs

@ -230,7 +230,7 @@ public class ContentQueryParser
entry.AbsoluteExpirationRelativeToNow = CacheTime; entry.AbsoluteExpirationRelativeToNow = CacheTime;
return ContentQueryModel.Build(schema?.SchemaDef, context.App.PartitionResolver(), components); return ContentQueryModel.Build(schema?.SchemaDef, context.App.PartitionResolver(), components);
}); })!;
return result; return result;
} }
@ -245,7 +245,7 @@ public class ContentQueryParser
entry.AbsoluteExpirationRelativeToNow = CacheTime; entry.AbsoluteExpirationRelativeToNow = CacheTime;
return BuildQueryModel(context, schema, components).ConvertToEdm("Contents", schema?.SchemaDef.Name ?? "Generic"); return BuildQueryModel(context, schema, components).ConvertToEdm("Contents", schema?.SchemaDef.Name ?? "Generic");
}); })!;
return result; return result;
} }

14
backend/src/Squidex.Domain.Apps.Entities/History/HistoryEventsCreatorBase.cs

@ -14,37 +14,37 @@ namespace Squidex.Domain.Apps.Entities.History;
public abstract class HistoryEventsCreatorBase : IHistoryEventsCreator public abstract class HistoryEventsCreatorBase : IHistoryEventsCreator
{ {
private readonly Dictionary<string, string> texts = new Dictionary<string, string>(); private readonly Dictionary<string, string> texts = new Dictionary<string, string>();
private readonly TypeNameRegistry typeNameRegistry; private readonly TypeRegistry typeRegistry;
public IReadOnlyDictionary<string, string> Texts public IReadOnlyDictionary<string, string> Texts
{ {
get => texts; get => texts;
} }
protected HistoryEventsCreatorBase(TypeNameRegistry typeNameRegistry) protected HistoryEventsCreatorBase(TypeRegistry typeRegistry)
{ {
Guard.NotNull(typeNameRegistry); Guard.NotNull(typeRegistry);
this.typeNameRegistry = typeNameRegistry; this.typeRegistry = typeRegistry;
} }
protected void AddEventMessage<TEvent>(string message) where TEvent : IEvent protected void AddEventMessage<TEvent>(string message) where TEvent : IEvent
{ {
Guard.NotNullOrEmpty(message); Guard.NotNullOrEmpty(message);
texts[typeNameRegistry.GetName<TEvent>()] = message; texts[typeRegistry.GetName<IEvent, TEvent>()] = message;
} }
protected bool HasEventText(IEvent @event) protected bool HasEventText(IEvent @event)
{ {
var message = typeNameRegistry.GetName(@event.GetType()); var message = typeRegistry.GetName<IEvent>(@event.GetType());
return texts.ContainsKey(message); return texts.ContainsKey(message);
} }
protected HistoryEvent ForEvent(IEvent @event, string channel) protected HistoryEvent ForEvent(IEvent @event, string channel)
{ {
var message = typeNameRegistry.GetName(@event.GetType()); var message = typeRegistry.GetName<IEvent>(@event.GetType());
return new HistoryEvent(channel, message); return new HistoryEvent(channel, message);
} }

2
backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs

@ -111,6 +111,6 @@ public sealed class RuleEnqueuer : IEventConsumer, IRuleEnqueuer
entry.AbsoluteExpirationRelativeToNow = cacheDuration; entry.AbsoluteExpirationRelativeToNow = cacheDuration;
return appProvider.GetRulesAsync(appId); return appProvider.GetRulesAsync(appId);
}); })!;
} }
} }

4
backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaHistoryEventsCreator.cs

@ -15,8 +15,8 @@ namespace Squidex.Domain.Apps.Entities.Schemas;
public sealed class SchemaHistoryEventsCreator : HistoryEventsCreatorBase public sealed class SchemaHistoryEventsCreator : HistoryEventsCreatorBase
{ {
public SchemaHistoryEventsCreator(TypeNameRegistry typeNameRegistry) public SchemaHistoryEventsCreator(TypeRegistry typeRegistry)
: base(typeNameRegistry) : base(typeRegistry)
{ {
AddEventMessage<SchemaFieldsReordered>( AddEventMessage<SchemaFieldsReordered>(
"history.schemas.fieldsReordered"); "history.schemas.fieldsReordered");

12
backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
<LangVersion>10.0</LangVersion> <LangVersion>10.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<NeutralLanguage>en</NeutralLanguage> <NeutralLanguage>en</NeutralLanguage>
@ -22,19 +22,19 @@
<PackageReference Include="CsvHelper" Version="30.0.0" /> <PackageReference Include="CsvHelper" Version="30.0.0" />
<PackageReference Include="GraphQL" Version="7.1.1" /> <PackageReference Include="GraphQL" Version="7.1.1" />
<PackageReference Include="GraphQL.DataLoader" Version="7.1.1" /> <PackageReference Include="GraphQL.DataLoader" Version="7.1.1" />
<PackageReference Include="Meziantou.Analyzer" Version="1.0.702"> <PackageReference Include="Meziantou.Analyzer" Version="1.0.746">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" /> <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
<PackageReference Include="Notifo.SDK" Version="1.2.0" /> <PackageReference Include="Notifo.SDK" Version="1.3.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.CLI.Core" Version="9.2.0" /> <PackageReference Include="Squidex.CLI.Core" Version="9.2.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="6.0.0" /> <PackageReference Include="System.Collections.Immutable" Version="7.0.0" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" /> <PackageReference Include="System.ValueTuple" Version="4.5.0" />
<PackageReference Include="taglib-sharp-netstandard2.0" Version="2.1.0" /> <PackageReference Include="taglib-sharp-netstandard2.0" Version="2.1.0" />
<PackageReference Include="tusdotnet" Version="2.6.0" /> <PackageReference Include="tusdotnet" Version="2.7.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<AdditionalFiles Include="..\..\stylecop.json" Link="stylecop.json" /> <AdditionalFiles Include="..\..\stylecop.json" Link="stylecop.json" />

4
backend/src/Squidex.Domain.Apps.Entities/Teams/TeamHistoryEventsCreator.cs

@ -14,8 +14,8 @@ namespace Squidex.Domain.Teams.Entities.Teams;
public sealed class TeamHistoryEventsCreator : HistoryEventsCreatorBase public sealed class TeamHistoryEventsCreator : HistoryEventsCreatorBase
{ {
public TeamHistoryEventsCreator(TypeNameRegistry typeNameRegistry) public TeamHistoryEventsCreator(TypeRegistry typeRegistry)
: base(typeNameRegistry) : base(typeRegistry)
{ {
AddEventMessage<TeamCreated>( AddEventMessage<TeamCreated>(
"history.teams.created"); "history.teams.created");

4
backend/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
<LangVersion>10.0</LangVersion> <LangVersion>10.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
@ -14,7 +14,7 @@
<ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" /> <ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="1.0.702"> <PackageReference Include="Meziantou.Analyzer" Version="1.0.746">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

12
backend/src/Squidex.Domain.Users.MongoDb/MongoRoleStore.cs

@ -62,13 +62,13 @@ public sealed class MongoRoleStore : MongoRepositoryBase<IdentityRole>, IRoleSto
{ {
} }
public async Task<IdentityRole> FindByIdAsync(string roleId, public async Task<IdentityRole?> FindByIdAsync(string roleId,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
return await Collection.Find(x => x.Id == roleId).FirstOrDefaultAsync(cancellationToken); return await Collection.Find(x => x.Id == roleId).FirstOrDefaultAsync(cancellationToken);
} }
public async Task<IdentityRole> FindByNameAsync(string normalizedRoleName, public async Task<IdentityRole?> FindByNameAsync(string normalizedRoleName,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
return await Collection.Find(x => x.NormalizedName == normalizedRoleName).FirstOrDefaultAsync(cancellationToken); return await Collection.Find(x => x.NormalizedName == normalizedRoleName).FirstOrDefaultAsync(cancellationToken);
@ -104,19 +104,19 @@ public sealed class MongoRoleStore : MongoRepositoryBase<IdentityRole>, IRoleSto
return Task.FromResult(role.Id); return Task.FromResult(role.Id);
} }
public Task<string> GetRoleNameAsync(IdentityRole role, public Task<string?> GetRoleNameAsync(IdentityRole role,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
return Task.FromResult(role.Name); return Task.FromResult(role.Name);
} }
public Task<string> GetNormalizedRoleNameAsync(IdentityRole role, public Task<string?> GetNormalizedRoleNameAsync(IdentityRole role,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
return Task.FromResult(role.NormalizedName); return Task.FromResult(role.NormalizedName);
} }
public Task SetRoleNameAsync(IdentityRole role, string roleName, public Task SetRoleNameAsync(IdentityRole role, string? roleName,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
role.Name = roleName; role.Name = roleName;
@ -124,7 +124,7 @@ public sealed class MongoRoleStore : MongoRepositoryBase<IdentityRole>, IRoleSto
return Task.CompletedTask; return Task.CompletedTask;
} }
public Task SetNormalizedRoleNameAsync(IdentityRole role, string normalizedName, public Task SetNormalizedRoleNameAsync(IdentityRole role, string? normalizedName,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
role.NormalizedName = normalizedName; role.NormalizedName = normalizedName;

14
backend/src/Squidex.Domain.Users.MongoDb/MongoUser.cs

@ -87,18 +87,24 @@ public sealed class MongoUser : IdentityUser
claims.Foreach(RemoveClaim); claims.Foreach(RemoveClaim);
} }
internal void ReplaceClaim(Claim existingClaim, Claim newClaim) internal void ReplaceClaim(Claim existingClaim, Claim? newClaim)
{ {
RemoveClaim(existingClaim); RemoveClaim(existingClaim);
AddClaim(newClaim); if (newClaim != null)
{
AddClaim(newClaim);
}
} }
internal void ReplaceToken(string provider, string name, string value) internal void ReplaceToken(string provider, string name, string? value)
{ {
RemoveToken(provider, name); RemoveToken(provider, name);
AddToken(provider, name, value); if (value != null)
{
AddToken(provider, name, value);
}
} }
} }

51
backend/src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs

@ -183,18 +183,15 @@ public sealed class MongoUserStore :
return new MongoUser { Email = email, UserName = email }; return new MongoUser { Email = email, UserName = email };
} }
public async Task<IdentityUser> FindByIdAsync(string userId, public async Task<IdentityUser?> FindByIdAsync(string userId,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
if (!IsId(userId)) var result = await Collection.Find(x => x.Id == userId).FirstOrDefaultAsync(cancellationToken);
{
return null!;
}
return await Collection.Find(x => x.Id == userId).FirstOrDefaultAsync(cancellationToken); return result;
} }
public async Task<IdentityUser> FindByEmailAsync(string normalizedEmail, public async Task<IdentityUser?> FindByEmailAsync(string normalizedEmail,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var result = await Collection.Find(x => x.NormalizedEmail == normalizedEmail).FirstOrDefaultAsync(cancellationToken); var result = await Collection.Find(x => x.NormalizedEmail == normalizedEmail).FirstOrDefaultAsync(cancellationToken);
@ -202,7 +199,7 @@ public sealed class MongoUserStore :
return result; return result;
} }
public async Task<IdentityUser> FindByNameAsync(string normalizedUserName, public async Task<IdentityUser?> FindByNameAsync(string normalizedUserName,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var result = await Collection.Find(x => x.NormalizedEmail == normalizedUserName).FirstOrDefaultAsync(cancellationToken); var result = await Collection.Find(x => x.NormalizedEmail == normalizedUserName).FirstOrDefaultAsync(cancellationToken);
@ -210,7 +207,7 @@ public sealed class MongoUserStore :
return result; return result;
} }
public async Task<IdentityUser> FindByLoginAsync(string loginProvider, string providerKey, public async Task<IdentityUser?> FindByLoginAsync(string loginProvider, string providerKey,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var result = await Collection.Find(x => x.Logins.Any(y => y.LoginProvider == loginProvider && y.ProviderKey == providerKey)).FirstOrDefaultAsync(cancellationToken); var result = await Collection.Find(x => x.Logins.Any(y => y.LoginProvider == loginProvider && y.ProviderKey == providerKey)).FirstOrDefaultAsync(cancellationToken);
@ -268,7 +265,7 @@ public sealed class MongoUserStore :
return Task.FromResult(result); return Task.FromResult(result);
} }
public Task<string> GetUserNameAsync(IdentityUser user, public Task<string?> GetUserNameAsync(IdentityUser user,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var result = user.UserName; var result = user.UserName;
@ -276,7 +273,7 @@ public sealed class MongoUserStore :
return Task.FromResult(result); return Task.FromResult(result);
} }
public Task<string> GetNormalizedUserNameAsync(IdentityUser user, public Task<string?> GetNormalizedUserNameAsync(IdentityUser user,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var result = user.NormalizedUserName; var result = user.NormalizedUserName;
@ -284,7 +281,7 @@ public sealed class MongoUserStore :
return Task.FromResult(result); return Task.FromResult(result);
} }
public Task<string> GetPasswordHashAsync(IdentityUser user, public Task<string?> GetPasswordHashAsync(IdentityUser user,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var result = user.PasswordHash; var result = user.PasswordHash;
@ -316,7 +313,7 @@ public sealed class MongoUserStore :
return Task.FromResult<IList<UserLoginInfo>>(result); return Task.FromResult<IList<UserLoginInfo>>(result);
} }
public Task<string> GetSecurityStampAsync(IdentityUser user, public Task<string?> GetSecurityStampAsync(IdentityUser user,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var result = user.SecurityStamp; var result = user.SecurityStamp;
@ -324,7 +321,7 @@ public sealed class MongoUserStore :
return Task.FromResult(result); return Task.FromResult(result);
} }
public Task<string> GetEmailAsync(IdentityUser user, public Task<string?> GetEmailAsync(IdentityUser user,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var result = user.Email; var result = user.Email;
@ -340,7 +337,7 @@ public sealed class MongoUserStore :
return Task.FromResult(result); return Task.FromResult(result);
} }
public Task<string> GetNormalizedEmailAsync(IdentityUser user, public Task<string?> GetNormalizedEmailAsync(IdentityUser user,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var result = user.NormalizedEmail; var result = user.NormalizedEmail;
@ -356,7 +353,7 @@ public sealed class MongoUserStore :
return Task.FromResult<IList<Claim>>(result); return Task.FromResult<IList<Claim>>(result);
} }
public Task<string> GetPhoneNumberAsync(IdentityUser user, public Task<string?> GetPhoneNumberAsync(IdentityUser user,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var result = user.PhoneNumber; var result = user.PhoneNumber;
@ -404,20 +401,20 @@ public sealed class MongoUserStore :
return Task.FromResult(result); return Task.FromResult(result);
} }
public Task<string> GetTokenAsync(IdentityUser user, string loginProvider, string name, public Task<string?> GetTokenAsync(IdentityUser user, string loginProvider, string name,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var result = ((MongoUser)user).GetToken(loginProvider, name)!; var result = ((MongoUser)user).GetToken(loginProvider, name)!;
return Task.FromResult(result); return Task.FromResult<string?>(result);
} }
public Task<string> GetAuthenticatorKeyAsync(IdentityUser user, public Task<string?> GetAuthenticatorKeyAsync(IdentityUser user,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var result = ((MongoUser)user).GetToken(InternalLoginProvider, AuthenticatorKeyTokenName)!; var result = ((MongoUser)user).GetToken(InternalLoginProvider, AuthenticatorKeyTokenName)!;
return Task.FromResult(result); return Task.FromResult<string?>(result);
} }
public Task<bool> HasPasswordAsync(IdentityUser user, public Task<bool> HasPasswordAsync(IdentityUser user,
@ -436,7 +433,7 @@ public sealed class MongoUserStore :
return Task.FromResult(result); return Task.FromResult(result);
} }
public Task SetUserNameAsync(IdentityUser user, string userName, public Task SetUserNameAsync(IdentityUser user, string? userName,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
((MongoUser)user).UserName = userName; ((MongoUser)user).UserName = userName;
@ -444,7 +441,7 @@ public sealed class MongoUserStore :
return Task.CompletedTask; return Task.CompletedTask;
} }
public Task SetNormalizedUserNameAsync(IdentityUser user, string normalizedName, public Task SetNormalizedUserNameAsync(IdentityUser user, string? normalizedName,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
((MongoUser)user).NormalizedUserName = normalizedName; ((MongoUser)user).NormalizedUserName = normalizedName;
@ -452,7 +449,7 @@ public sealed class MongoUserStore :
return Task.CompletedTask; return Task.CompletedTask;
} }
public Task SetPasswordHashAsync(IdentityUser user, string passwordHash, public Task SetPasswordHashAsync(IdentityUser user, string? passwordHash,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
((MongoUser)user).PasswordHash = passwordHash; ((MongoUser)user).PasswordHash = passwordHash;
@ -500,7 +497,7 @@ public sealed class MongoUserStore :
return Task.CompletedTask; return Task.CompletedTask;
} }
public Task SetEmailAsync(IdentityUser user, string email, public Task SetEmailAsync(IdentityUser user, string? email,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
((MongoUser)user).Email = email; ((MongoUser)user).Email = email;
@ -516,7 +513,7 @@ public sealed class MongoUserStore :
return Task.CompletedTask; return Task.CompletedTask;
} }
public Task SetNormalizedEmailAsync(IdentityUser user, string normalizedEmail, public Task SetNormalizedEmailAsync(IdentityUser user, string? normalizedEmail,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
((MongoUser)user).NormalizedEmail = normalizedEmail; ((MongoUser)user).NormalizedEmail = normalizedEmail;
@ -548,7 +545,7 @@ public sealed class MongoUserStore :
return Task.CompletedTask; return Task.CompletedTask;
} }
public Task SetPhoneNumberAsync(IdentityUser user, string phoneNumber, public Task SetPhoneNumberAsync(IdentityUser user, string? phoneNumber,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
((MongoUser)user).PhoneNumber = phoneNumber; ((MongoUser)user).PhoneNumber = phoneNumber;
@ -604,7 +601,7 @@ public sealed class MongoUserStore :
return Task.CompletedTask; return Task.CompletedTask;
} }
public Task SetTokenAsync(IdentityUser user, string loginProvider, string name, string value, public Task SetTokenAsync(IdentityUser user, string loginProvider, string name, string? value,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
((MongoUser)user).ReplaceToken(loginProvider, name, value); ((MongoUser)user).ReplaceToken(loginProvider, name, value);

6
backend/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
<LangVersion>10.0</LangVersion> <LangVersion>10.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
@ -19,12 +19,12 @@
<ProjectReference Include="..\Squidex.Shared\Squidex.Shared.csproj" /> <ProjectReference Include="..\Squidex.Shared\Squidex.Shared.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="1.0.702"> <PackageReference Include="Meziantou.Analyzer" Version="1.0.746">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" /> <PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
<PackageReference Include="MongoDB.Driver" Version="2.17.1" /> <PackageReference Include="MongoDB.Driver" Version="2.18.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Security.Principal.Windows" Version="5.0.0" /> <PackageReference Include="System.Security.Principal.Windows" Version="5.0.0" />

6
backend/src/Squidex.Domain.Users/DefaultUserService.cs

@ -46,7 +46,7 @@ public sealed class DefaultUserService : IUserService
{ {
Guard.NotNull(user); Guard.NotNull(user);
return userManager.GetUserId(user); return userManager.GetUserId(user)!;
} }
public async Task<IResultList<IUser>> QueryAsync(IEnumerable<string> ids, public async Task<IResultList<IUser>> QueryAsync(IEnumerable<string> ids,
@ -82,7 +82,7 @@ public sealed class DefaultUserService : IUserService
{ {
var normalizedEmail = userManager.NormalizeEmail(email); var normalizedEmail = userManager.NormalizeEmail(email);
result = result.Where(x => x.NormalizedEmail.Contains(normalizedEmail)); result = result.Where(x => x.NormalizedEmail!.Contains(normalizedEmail));
} }
return result; return result;
@ -387,7 +387,7 @@ public sealed class DefaultUserService : IUserService
if (!claims.Any(x => string.Equals(x.Type, SquidexClaimTypes.DisplayName, StringComparison.OrdinalIgnoreCase))) if (!claims.Any(x => string.Equals(x.Type, SquidexClaimTypes.DisplayName, StringComparison.OrdinalIgnoreCase)))
{ {
claims.Add(new Claim(SquidexClaimTypes.DisplayName, user.Email)); claims.Add(new Claim(SquidexClaimTypes.DisplayName, user.Email!));
} }
return new UserWithClaims(user, claims.ToList()); return new UserWithClaims(user, claims.ToList());

6
backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
<LangVersion>10.0</LangVersion> <LangVersion>10.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
@ -18,11 +18,11 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="IdentityModel" Version="6.0.0" /> <PackageReference Include="IdentityModel" Version="6.0.0" />
<PackageReference Include="Meziantou.Analyzer" Version="1.0.702"> <PackageReference Include="Meziantou.Analyzer" Version="1.0.746">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="6.0.5" /> <PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="7.0.0" />
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" /> <PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
<PackageReference Include="OpenIddict.AspNetCore" Version="3.1.1" /> <PackageReference Include="OpenIddict.AspNetCore" Version="3.1.1" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />

14
backend/src/Squidex.Domain.Users/UserClaimsPrincipalFactoryWithEmail.cs

@ -25,17 +25,21 @@ public sealed class UserClaimsPrincipalFactoryWithEmail : UserClaimsPrincipalFac
public override async Task<ClaimsPrincipal> CreateAsync(IdentityUser user) public override async Task<ClaimsPrincipal> CreateAsync(IdentityUser user)
{ {
var principal = await base.CreateAsync(user); var userPrincipal = await base.CreateAsync(user);
var userIdentity = userPrincipal.Identities.First();
var identity = principal.Identities.First(); var email = await UserManager.GetEmailAsync(user);
identity.AddClaim(new Claim(OpenIdClaims.Email, await UserManager.GetEmailAsync(user))); if (email != null)
{
userIdentity.AddClaim(new Claim(OpenIdClaims.Email, email));
}
if (await UserManager.IsInRoleAsync(user, AdministratorRole)) if (await UserManager.IsInRoleAsync(user, AdministratorRole))
{ {
identity.AddClaim(new Claim(SquidexClaimTypes.Permissions, PermissionIds.Admin)); userIdentity.AddClaim(new Claim(SquidexClaimTypes.Permissions, PermissionIds.Admin));
} }
return principal; return userPrincipal;
} }
} }

2
backend/src/Squidex.Domain.Users/UserWithClaims.cs

@ -22,7 +22,7 @@ internal sealed class UserWithClaims : IUser
public string Email public string Email
{ {
get => Identity.Email; get => Identity.Email!;
} }
public bool IsLocked public bool IsLocked

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

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
<RootNamespace>Squidex.Infrastructure</RootNamespace> <RootNamespace>Squidex.Infrastructure</RootNamespace>
<LangVersion>10.0</LangVersion> <LangVersion>10.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>

6
backend/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
<RootNamespace>Squidex.Infrastructure</RootNamespace> <RootNamespace>Squidex.Infrastructure</RootNamespace>
<LangVersion>10.0</LangVersion> <LangVersion>10.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
@ -14,8 +14,8 @@
<PackageReference Include="EventStore.Client.Grpc.PersistentSubscriptions" Version="22.0.0" /> <PackageReference Include="EventStore.Client.Grpc.PersistentSubscriptions" Version="22.0.0" />
<PackageReference Include="EventStore.Client.Grpc.ProjectionManagement" Version="22.0.0" /> <PackageReference Include="EventStore.Client.Grpc.ProjectionManagement" Version="22.0.0" />
<PackageReference Include="EventStore.Client.Grpc.Streams" Version="22.0.0" /> <PackageReference Include="EventStore.Client.Grpc.Streams" Version="22.0.0" />
<PackageReference Include="Grpc.Net.Client" Version="2.46.0" /> <PackageReference Include="Grpc.Net.Client" Version="2.49.0" />
<PackageReference Include="Meziantou.Analyzer" Version="1.0.702"> <PackageReference Include="Meziantou.Analyzer" Version="1.0.746">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

10
backend/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
<RootNamespace>Squidex.Infrastructure</RootNamespace> <RootNamespace>Squidex.Infrastructure</RootNamespace>
<LangVersion>10.0</LangVersion> <LangVersion>10.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
@ -14,15 +14,15 @@
<ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" /> <ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="1.0.702"> <PackageReference Include="Meziantou.Analyzer" Version="1.0.746">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="MongoDB.Driver" Version="2.17.1" /> <PackageReference Include="MongoDB.Driver" Version="2.18.0" />
<PackageReference Include="MongoDB.Driver.GridFS" Version="2.17.1" /> <PackageReference Include="MongoDB.Driver.GridFS" Version="2.18.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Threading.Tasks.Dataflow" Version="6.0.0" /> <PackageReference Include="System.Threading.Tasks.Dataflow" Version="7.0.0" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" /> <PackageReference Include="System.ValueTuple" Version="4.5.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

4
backend/src/Squidex.Infrastructure.RabbitMq/Squidex.Infrastructure.RabbitMq.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
<RootNamespace>Squidex.Infrastructure</RootNamespace> <RootNamespace>Squidex.Infrastructure</RootNamespace>
<LangVersion>10.0</LangVersion> <LangVersion>10.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
@ -11,7 +11,7 @@
<DebugSymbols>True</DebugSymbols> <DebugSymbols>True</DebugSymbols>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="1.0.702"> <PackageReference Include="Meziantou.Analyzer" Version="1.0.746">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

2
backend/src/Squidex.Infrastructure.Redis/Squidex.Infrastructure.Redis.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
<RootNamespace>Squidex.Infrastructure</RootNamespace> <RootNamespace>Squidex.Infrastructure</RootNamespace>
<LangVersion>10.0</LangVersion> <LangVersion>10.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>

29
backend/src/Squidex.Infrastructure/EventSourcing/DefaultEventFormatter.cs

@ -14,11 +14,11 @@ namespace Squidex.Infrastructure.EventSourcing;
public sealed class DefaultEventFormatter : IEventFormatter public sealed class DefaultEventFormatter : IEventFormatter
{ {
private readonly IJsonSerializer serializer; private readonly IJsonSerializer serializer;
private readonly TypeNameRegistry typeNameRegistry; private readonly TypeRegistry typeRegistry;
public DefaultEventFormatter(TypeNameRegistry typeNameRegistry, IJsonSerializer serializer) public DefaultEventFormatter(TypeRegistry typeRegistry, IJsonSerializer serializer)
{ {
this.typeNameRegistry = typeNameRegistry; this.typeRegistry = typeRegistry;
this.serializer = serializer; this.serializer = serializer;
} }
@ -33,7 +33,8 @@ public sealed class DefaultEventFormatter : IEventFormatter
if (envelope == null) if (envelope == null)
{ {
throw new TypeNameNotFoundException($"Cannot find event with type name '{storedEvent.Data.Type}'."); ThrowHelper.InvalidOperationException($"Cannot find event with type name '{storedEvent.Data.Type}'.");
return default!;
} }
return envelope; return envelope;
@ -43,21 +44,19 @@ public sealed class DefaultEventFormatter : IEventFormatter
{ {
Guard.NotNull(storedEvent); Guard.NotNull(storedEvent);
var payloadType = typeNameRegistry.GetTypeOrNull(storedEvent.Data.Type); if (!typeRegistry.TryGetType<IEvent>(storedEvent.Data.Type, out var type))
if (payloadType == null)
{ {
return null; return null;
} }
var payloadValue = serializer.Deserialize<IEvent>(storedEvent.Data.Payload, payloadType); var payload = serializer.Deserialize<IEvent>(storedEvent.Data.Payload, type);
if (payloadValue is IMigrated<IEvent> migratedEvent) if (payload is IMigrated<IEvent> migratedEvent)
{ {
payloadValue = migratedEvent.Migrate(); payload = migratedEvent.Migrate();
} }
var envelope = new Envelope<IEvent>(payloadValue, storedEvent.Data.Headers); var envelope = new Envelope<IEvent>(payload, storedEvent.Data.Headers);
envelope.SetEventPosition(storedEvent.EventPosition); envelope.SetEventPosition(storedEvent.EventPosition);
envelope.SetEventStreamNumber(storedEvent.EventStreamNumber); envelope.SetEventStreamNumber(storedEvent.EventStreamNumber);
@ -67,14 +66,14 @@ public sealed class DefaultEventFormatter : IEventFormatter
public EventData ToEventData(Envelope<IEvent> envelope, Guid commitId, bool migrate = true) public EventData ToEventData(Envelope<IEvent> envelope, Guid commitId, bool migrate = true)
{ {
var payloadValue = envelope.Payload; var payload = envelope.Payload;
if (migrate && payloadValue is IMigrated<IEvent> migratedEvent) if (migrate && payload is IMigrated<IEvent> migratedEvent)
{ {
payloadValue = migratedEvent.Migrate(); payload = migratedEvent.Migrate();
} }
var payloadType = typeNameRegistry.GetName(payloadValue.GetType()); var payloadType = typeRegistry.GetName<IEvent>(payload.GetType());
var payloadJson = serializer.Serialize(envelope.Payload, envelope.Payload.GetType()); var payloadJson = serializer.Serialize(envelope.Payload, envelope.Payload.GetType());
envelope.SetCommitId(commitId); envelope.SetCommitId(commitId);

14
backend/src/Squidex.Infrastructure/EventSourcing/EventTypeAttribute.cs

@ -12,6 +12,8 @@ namespace Squidex.Infrastructure.EventSourcing;
[AttributeUsage(AttributeTargets.Class, Inherited = false)] [AttributeUsage(AttributeTargets.Class, Inherited = false)]
public sealed class EventTypeAttribute : TypeNameAttribute public sealed class EventTypeAttribute : TypeNameAttribute
{ {
private const string Suffix = "Event";
public EventTypeAttribute(string typeName, int version = 1) public EventTypeAttribute(string typeName, int version = 1)
: base(CreateTypeName(typeName, version)) : base(CreateTypeName(typeName, version))
{ {
@ -19,6 +21,16 @@ public sealed class EventTypeAttribute : TypeNameAttribute
private static string CreateTypeName(string typeName, int version) private static string CreateTypeName(string typeName, int version)
{ {
return $"{typeName}Event" + (version > 1 ? $"V{version}" : string.Empty); if (!typeName.EndsWith(Suffix, StringComparison.OrdinalIgnoreCase))
{
typeName += Suffix;
}
if (version > 1)
{
typeName = $"{typeName}V{version}";
}
return typeName;
} }
} }

19
backend/src/Squidex.Infrastructure/GravatarHelper.cs

@ -29,19 +29,16 @@ public static class GravatarHelper
private static string Hash(string email) private static string Hash(string email)
{ {
using (var md5 = MD5.Create()) var normalizedEmail = email.ToLowerInvariant().Trim();
{
var normalizedEmail = email.ToLowerInvariant().Trim();
var hashBytes = md5.ComputeHash(Encoding.UTF8.GetBytes(normalizedEmail));
var hashBuilder = new StringBuilder();
for (var i = 0; i < hashBytes.Length; i++) var hashBytes = MD5.HashData(Encoding.UTF8.GetBytes(normalizedEmail));
{ var hashBuilder = new StringBuilder();
hashBuilder.Append(hashBytes[i].ToString("x2", CultureInfo.InvariantCulture));
}
return hashBuilder.ToString(); for (var i = 0; i < hashBytes.Length; i++)
{
hashBuilder.Append(hashBytes[i].ToString("x2", CultureInfo.InvariantCulture));
} }
return hashBuilder.ToString();
} }
} }

13
backend/src/Squidex.Infrastructure/Reflection/AutoAssembyTypeProvider.cs → backend/src/Squidex.Infrastructure/Json/System/Constants.cs

@ -5,12 +5,13 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
namespace Squidex.Infrastructure.Reflection; using System.Text.Json;
public sealed class AutoAssembyTypeProvider<T> : ITypeProvider namespace Squidex.Infrastructure.Json.System;
internal sealed class Constants
{ {
public void Map(TypeNameRegistry typeNameRegistry) public const string DefaultDiscriminatorProperty = "$type";
{
typeNameRegistry.MapUnmapped(typeof(T).Assembly); public static readonly JsonEncodedText DefaultDiscriminiatorPropertyJson = JsonEncodedText.Encode("$type");
}
} }

52
backend/src/Squidex.Infrastructure/Json/System/InheritanceConverter.cs

@ -1,52 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure.Reflection;
namespace Squidex.Infrastructure.Json.System;
public sealed class InheritanceConverter<T> : InheritanceConverterBase<T> where T : notnull
{
private readonly TypeNameRegistry typeNameRegistry;
public InheritanceConverter(TypeNameRegistry typeNameRegistry)
: base("$type")
{
this.typeNameRegistry = typeNameRegistry;
}
public override Type GetDiscriminatorType(string name, Type typeToConvert)
{
var typeInfo = typeNameRegistry.GetTypeOrNull(name);
if (typeInfo == null)
{
typeInfo = Type.GetType(name);
}
if (typeInfo == null)
{
ThrowHelper.JsonException($"Object has invalid discriminator '{name}'.");
return default!;
}
return typeInfo;
}
public override string GetDiscriminatorValue(Type type)
{
var typeName = typeNameRegistry.GetNameOrNull(type);
if (typeName == null)
{
// Use the type name as a fallback.
typeName = type.AssemblyQualifiedName!;
}
return typeName;
}
}

94
backend/src/Squidex.Infrastructure/Json/System/InheritanceConverterBase.cs

@ -1,94 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Text.Json;
using System.Text.Json.Serialization;
using Squidex.Infrastructure.ObjectPool;
namespace Squidex.Infrastructure.Json.System;
public abstract class InheritanceConverterBase<T> : JsonConverter<T> where T : notnull
{
private readonly JsonEncodedText discriminatorProperty;
public string DiscriminatorName { get; }
protected InheritanceConverterBase(string discriminatorName)
{
discriminatorProperty = JsonEncodedText.Encode(discriminatorName);
DiscriminatorName = discriminatorName;
}
public abstract Type GetDiscriminatorType(string name, Type typeToConvert);
public abstract string GetDiscriminatorValue(Type type);
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
// Creating a copy of the reader (The derived deserialisation has to be done from the start)
Utf8JsonReader typeReader = reader;
if (typeReader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
if (!typeReader.Read() || typeReader.TokenType != JsonTokenType.PropertyName)
{
throw new JsonException();
}
var propertyName = typeReader.GetString();
if (typeReader.Read() && typeReader.TokenType == JsonTokenType.String && propertyName == DiscriminatorName)
{
var type = GetDiscriminatorType(typeReader.GetString()!, typeToConvert);
return (T?)JsonSerializer.Deserialize(ref reader, type, options);
}
else
{
using var document = JsonDocument.ParseValue(ref reader);
if (!document.RootElement.TryGetProperty(DiscriminatorName, out var discriminator))
{
ThrowHelper.JsonException($"Object has no discriminator '{DiscriminatorName}.");
return default!;
}
var type = GetDiscriminatorType(discriminator.GetString()!, typeToConvert);
using var bufferWriter = DefaultPools.MemoryStream.GetStream();
using (var writer = new Utf8JsonWriter(bufferWriter))
{
document.RootElement.WriteTo(writer);
}
return (T?)JsonSerializer.Deserialize(bufferWriter.ToArray(), type, options);
}
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
var name = GetDiscriminatorValue(value.GetType());
writer.WriteStartObject();
writer.WriteString(discriminatorProperty, name);
using (var document = JsonSerializer.SerializeToDocument(value, value.GetType(), options))
{
foreach (var property in document.RootElement.EnumerateObject())
{
property.WriteTo(writer);
}
}
writer.WriteEndObject();
}
}

104
backend/src/Squidex.Infrastructure/Json/System/PolymorphicConverter.cs

@ -0,0 +1,104 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Text.Json;
using System.Text.Json.Serialization;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Infrastructure.Json.System;
public sealed class PolymorphicConverter<T> : JsonConverter<T> where T : class
{
private readonly JsonEncodedText discriminatorProperty;
private readonly string discriminatorName;
private readonly TypeRegistry typeRegistry;
public PolymorphicConverter(TypeRegistry typeRegistry)
{
this.typeRegistry = typeRegistry;
typeRegistry.TryGetConfig<T>(out var config);
discriminatorName = config?.DiscriminatorProperty ?? Constants.DefaultDiscriminatorProperty;
discriminatorProperty = JsonEncodedText.Encode(discriminatorName);
}
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
// Creating a copy of the reader (The derived deserialisation has to be done from the start)
Utf8JsonReader typeReader = reader;
if (typeReader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
while (typeReader.Read())
{
if (typeReader.TokenType == JsonTokenType.PropertyName && IsDiscriminiator(typeReader))
{
// Advance the reader to the property value
typeReader.Read();
if (typeReader.TokenType != JsonTokenType.String)
{
ThrowHelper.JsonException($"Expected string discriminator value, got '{reader.TokenType}'");
return default!;
}
// Resolve the type from the discriminator value.
var type = GetDiscriminatorType(typeReader.GetString()!);
// Perform the actual deserialization with the original reader
return (T)JsonSerializer.Deserialize(ref reader, type, options)!;
}
else if (typeReader.TokenType == JsonTokenType.StartObject || typeReader.TokenType == JsonTokenType.StartArray)
{
if (!typeReader.TrySkip())
{
typeReader.Skip();
}
}
}
ThrowHelper.JsonException($"Object has no discriminator '{discriminatorName}.");
return default!;
}
private bool IsDiscriminiator(Utf8JsonReader typeReader)
{
return
typeReader.ValueTextEquals(discriminatorProperty.EncodedUtf8Bytes) ||
typeReader.ValueTextEquals(Constants.DefaultDiscriminatorProperty);
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
EnsureTypeResolver(options);
JsonSerializer.Serialize<object>(writer, value!, options);
}
private static void EnsureTypeResolver(JsonSerializerOptions options)
{
if (options.TypeInfoResolver is not PolymorphicTypeResolver)
{
ThrowHelper.JsonException($"TypeInfoResolver must be of type PolymorphicTypeResolver.");
}
}
private Type GetDiscriminatorType(string name)
{
if (!typeRegistry.TryGetType<T>(name, out var type))
{
ThrowHelper.JsonException($"Object has invalid discriminator '{name}'.");
return default!;
}
return type;
}
}

51
backend/src/Squidex.Infrastructure/Json/System/PolymorphicTypeResolver.cs

@ -0,0 +1,51 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Infrastructure.Json.System;
public sealed class PolymorphicTypeResolver : DefaultJsonTypeInfoResolver
{
private readonly TypeRegistry typeRegistry;
public PolymorphicTypeResolver(TypeRegistry typeRegistry)
{
Guard.NotNull(typeRegistry);
this.typeRegistry = typeRegistry;
}
public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options)
{
var typeInfo = base.GetTypeInfo(type, options);
var baseType = type.BaseType;
while (baseType != null)
{
if (typeRegistry.TryGetConfig(baseType, out var config) && config.TryGetName(type, out var typeName))
{
var discriminiatorName = config.DiscriminatorProperty ?? Constants.DefaultDiscriminatorProperty;
var discriminatorField = typeInfo.CreateJsonPropertyInfo(typeof(string), discriminiatorName);
discriminatorField.Get = x =>
{
return typeName;
};
typeInfo.Properties.Insert(0, discriminatorField);
}
baseType = baseType.BaseType;
}
return typeInfo;
}
}

5
backend/src/Squidex.Infrastructure/Json/System/ReflectionHelper.cs

@ -42,4 +42,9 @@ internal static class ReflectionHelper
return dynamicMethod; return dynamicMethod;
} }
public static string GetShortTypeName(this Type type)
{
return $"{type.FullName}, {type.Assembly.GetName().Name}";
}
} }

33
backend/src/Squidex.Infrastructure/Json/System/StringConverter.cs

@ -6,6 +6,7 @@
// ========================================================================== // ==========================================================================
using System.ComponentModel; using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
@ -47,10 +48,7 @@ public sealed class StringConverter<T> : JsonConverter<T> where T : notnull
} }
case JsonTokenType.StartObject: case JsonTokenType.StartObject:
var optionsWithoutSelf = new JsonSerializerOptions(options); var optionsWithoutSelf = OptionClones.GetOptionsWithoutConverter(options, this);
// Remove the current converter, otherwise we would create a stackoverflow exception.
optionsWithoutSelf.Converters.Remove(this);
return JsonSerializer.Deserialize<T>(ref reader, optionsWithoutSelf); return JsonSerializer.Deserialize<T>(ref reader, optionsWithoutSelf);
@ -75,3 +73,30 @@ public sealed class StringConverter<T> : JsonConverter<T> where T : notnull
writer.WritePropertyName(convertToString(value)!); writer.WritePropertyName(convertToString(value)!);
} }
} }
#pragma warning disable MA0048 // File name must match type name
internal static class OptionClones
#pragma warning restore MA0048 // File name must match type name
{
private static readonly ConditionalWeakTable<JsonSerializerOptions, JsonSerializerOptions> Clones = new ConditionalWeakTable<JsonSerializerOptions, JsonSerializerOptions>();
public static JsonSerializerOptions GetOptionsWithoutConverter(JsonSerializerOptions source, JsonConverter converter)
{
if (!source.Converters.Contains(converter))
{
return source;
}
if (!Clones.TryGetValue(source, out var clone))
{
clone = new JsonSerializerOptions(source);
// Remove the current converter, otherwise we would create a stackoverflow exception.
clone.Converters.Remove(converter);
Clones.TryAdd(source, clone);
}
return clone;
}
}

17
backend/src/Squidex.Infrastructure/NamedId{T}.cs

@ -9,29 +9,18 @@ using System.ComponentModel;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
#pragma warning disable MA0048 // File name must match type name #pragma warning disable MA0048 // File name must match type name
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
namespace Squidex.Infrastructure; namespace Squidex.Infrastructure;
public delegate bool Parser<T>(ReadOnlySpan<char> input, out T result); public delegate bool Parser<T>(ReadOnlySpan<char> input, out T result);
[TypeConverter(typeof(NamedIdTypeConverter))] [TypeConverter(typeof(NamedIdTypeConverter))]
public sealed record NamedId<T> where T : notnull public sealed record NamedId<T>(T Id, string Name) where T : notnull
{ {
private static readonly int GuidLength = Guid.Empty.ToString().Length; private static readonly int GuidLength = Guid.Empty.ToString().Length;
public T Id { get; } public string Name { get; init; } = Guard.NotNullOrEmpty(Name);
public string Name { get; }
public NamedId(T id, string name)
{
Guard.NotNull(id);
Guard.NotNull(name);
Id = id;
Name = name;
}
public override string ToString() public override string ToString()
{ {

9
backend/src/Squidex.Infrastructure/RandomHash.cs

@ -34,14 +34,11 @@ public static class RandomHash
public static string ToSha256Base64(this byte[] bytes) public static string ToSha256Base64(this byte[] bytes)
{ {
using (var sha = SHA256.Create()) var hashBytes = SHA256.HashData(bytes);
{
var bytesHash = sha.ComputeHash(bytes);
var result = Convert.ToBase64String(bytesHash); var result = Convert.ToBase64String(hashBytes);
return result; return result;
}
} }
public static string ToSha256(this string value) public static string ToSha256(this string value)

53
backend/src/Squidex.Infrastructure/Reflection/AssemblyTypeProvider.cs

@ -0,0 +1,53 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Reflection;
namespace Squidex.Infrastructure.Reflection;
public sealed class AssemblyTypeProvider<T> : ITypeProvider where T : class
{
private readonly Assembly assembly;
public string? DiscriminatorProperty { get; }
public AssemblyTypeProvider(string? discriminatorProperty = null)
: this(typeof(T).Assembly, discriminatorProperty)
{
}
public AssemblyTypeProvider(Assembly assembly, string? discriminatorProperty = null)
{
Guard.NotNull(assembly);
this.assembly = assembly;
DiscriminatorProperty = discriminatorProperty;
}
public void Map(TypeRegistry typeRegistry)
{
var baseType = typeof(T);
foreach (var derivedType in assembly.GetTypes())
{
if (derivedType.IsAssignableTo(baseType) && !derivedType.IsAbstract)
{
var typeName = derivedType.GetCustomAttribute<TypeNameAttribute>()?.TypeName;
if (string.IsNullOrWhiteSpace(typeName))
{
typeName = derivedType.TypeName(false, baseType.Name);
}
typeRegistry.Add<T>(derivedType, typeName);
}
}
typeRegistry.Discriminator<T>(DiscriminatorProperty);
}
}

2
backend/src/Squidex.Infrastructure/Reflection/ITypeProvider.cs

@ -9,5 +9,5 @@ namespace Squidex.Infrastructure.Reflection;
public interface ITypeProvider public interface ITypeProvider
{ {
void Map(TypeNameRegistry typeNameRegistry); void Map(TypeRegistry typeRegistry);
} }

13
backend/src/Squidex.Infrastructure/Reflection/IgnoreEqualsAttribute.cs

@ -1,13 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Infrastructure.Reflection;
[AttributeUsage(AttributeTargets.Property)]
public sealed class IgnoreEqualsAttribute : Attribute
{
}

83
backend/src/Squidex.Infrastructure/Reflection/TypeConfig.cs

@ -0,0 +1,83 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Diagnostics.CodeAnalysis;
namespace Squidex.Infrastructure.Reflection;
public sealed class TypeConfig
{
private readonly List<(Type DerivedType, string TypeName)> derivedTypes = new List<(Type DervicedType, string TypeName)>();
private Dictionary<string, Type>? mapByName;
private Dictionary<Type, string>? mapByType;
public string? DiscriminatorProperty { get; set; }
public IReadOnlyList<(Type DerivedType, string TypeName)> DerivedTypes
{
get => derivedTypes;
}
internal void Add(Type derivedType, string typeName)
{
Guard.NotNull(derivedType);
Guard.NotNullOrEmpty(typeName);
if (!derivedTypes.Contains((derivedType, typeName)))
{
derivedTypes.Add((derivedType, typeName));
}
var (conflict, _) = derivedTypes.Find(x => x.TypeName == typeName && x.DerivedType != derivedType);
if (conflict != null)
{
ThrowHelper.ArgumentException($"Type name '{typeName}' is already used by type '{conflict}", nameof(typeName));
}
mapByName = null;
mapByType = null;
}
public bool TryGetType(string typeName, [MaybeNullWhen(false)] out Type derivedType)
{
var map = mapByName ??= BuildMapByName();
return map.TryGetValue(typeName, out derivedType);
}
public bool TryGetName(Type derivedType, [MaybeNullWhen(false)] out string typeName)
{
var map = mapByType ??= BuildMapByType();
return map.TryGetValue(derivedType, out typeName);
}
private Dictionary<string, Type> BuildMapByName()
{
var result = new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase);
foreach (var (derivedType, typeName) in derivedTypes)
{
result[typeName] = derivedType;
}
return result;
}
private Dictionary<Type, string> BuildMapByType()
{
var result = new Dictionary<Type, string>();
foreach (var (derivedType, typeName) in derivedTypes)
{
result[derivedType] = typeName;
}
return result;
}
}

1
backend/src/Squidex.Infrastructure/Reflection/TypeNameBuilder.cs

@ -22,7 +22,6 @@ public static class TypeNameBuilder
if (typeName.EndsWith(suffix, StringComparison.Ordinal)) if (typeName.EndsWith(suffix, StringComparison.Ordinal))
{ {
typeName = typeName[..^suffix.Length]; typeName = typeName[..^suffix.Length];
break; break;
} }
} }

24
backend/src/Squidex.Infrastructure/Reflection/TypeNameNotFoundException.cs

@ -1,24 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Runtime.Serialization;
namespace Squidex.Infrastructure.Reflection;
[Serializable]
public class TypeNameNotFoundException : Exception
{
public TypeNameNotFoundException(string? message = null, Exception? inner = null)
: base(message, inner)
{
}
protected TypeNameNotFoundException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}

156
backend/src/Squidex.Infrastructure/Reflection/TypeNameRegistry.cs

@ -1,156 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Reflection;
namespace Squidex.Infrastructure.Reflection;
public sealed class TypeNameRegistry
{
private readonly Dictionary<Type, string> namesByType = new Dictionary<Type, string>();
private readonly Dictionary<string, Type> typesByName = new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase);
public TypeNameRegistry(IEnumerable<ITypeProvider>? providers = null)
{
if (providers != null)
{
foreach (var provider in providers)
{
Map(provider);
}
}
}
public TypeNameRegistry MapObsolete(Type type, string name)
{
Guard.NotNull(type);
Guard.NotNull(name);
lock (namesByType)
{
if (typesByName.TryGetValue(name, out var existingType) && existingType != type)
{
var message = $"The name '{name}' is already registered with type '{typesByName[name]}'";
ThrowHelper.ArgumentException(message, nameof(type));
}
typesByName[name] = type;
}
return this;
}
public TypeNameRegistry Map(ITypeProvider provider)
{
Guard.NotNull(provider);
provider.Map(this);
return this;
}
public TypeNameRegistry Map(Type type)
{
Guard.NotNull(type);
var typeNameAttribute = type.GetCustomAttribute<TypeNameAttribute>();
if (!string.IsNullOrWhiteSpace(typeNameAttribute?.TypeName))
{
Map(type, typeNameAttribute.TypeName);
}
return this;
}
public TypeNameRegistry Map(Type type, string name)
{
Guard.NotNull(type);
Guard.NotNull(name);
lock (namesByType)
{
if (namesByType.TryGetValue(type, out var existingName) && existingName != name)
{
var message = $"The type '{type}' is already registered with name '{namesByType[type]}'";
ThrowHelper.ArgumentException(message, nameof(type));
}
namesByType[type] = name;
if (typesByName.TryGetValue(name, out var existingType) && existingType != type)
{
var message = $"The name '{name}' is already registered with type '{typesByName[name]}'";
ThrowHelper.ArgumentException(message, nameof(type));
}
typesByName[name] = type;
}
return this;
}
public TypeNameRegistry MapUnmapped(Assembly assembly)
{
foreach (var type in assembly.GetTypes())
{
if (!namesByType.ContainsKey(type))
{
Map(type);
}
}
return this;
}
public string GetName<T>()
{
return GetName(typeof(T));
}
public string? GetNameOrNull<T>()
{
return GetNameOrNull(typeof(T));
}
public string? GetNameOrNull(Type type)
{
return namesByType.GetValueOrDefault(type);
}
public Type? GetTypeOrNull(string name)
{
return typesByName.GetValueOrDefault(name);
}
public string GetName(Type type)
{
var result = namesByType.GetValueOrDefault(type);
if (result == null)
{
throw new TypeNameNotFoundException($"There is no name for type '{type}");
}
return result;
}
public Type GetType(string name)
{
var result = typesByName.GetValueOrDefault(name);
if (result == null)
{
throw new TypeNameNotFoundException($"There is no type for name '{name}");
}
return result;
}
}

111
backend/src/Squidex.Infrastructure/Reflection/TypeRegistry.cs

@ -0,0 +1,111 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Diagnostics.CodeAnalysis;
namespace Squidex.Infrastructure.Reflection;
public sealed class TypeRegistry
{
private readonly Dictionary<Type, TypeConfig> configs = new Dictionary<Type, TypeConfig>();
public TypeRegistry(IEnumerable<ITypeProvider>? providers = null)
{
if (providers != null)
{
foreach (var provider in providers)
{
Map(provider);
}
}
}
public TypeRegistry Map(ITypeProvider provider)
{
Guard.NotNull(provider);
provider.Map(this);
return this;
}
public TypeRegistry Add<TBase, TDerived>(string typeName) where TDerived : TBase where TBase : class
{
return Add<TBase>(typeof(TDerived), typeName);
}
public TypeRegistry Add<T>(Type derivedType, string typeName) where T : class
{
lock (configs)
{
configs.GetOrAddNew(typeof(T)).Add(derivedType, typeName);
}
return this;
}
public TypeRegistry Discriminator<T>(string? discriminiatorProperty)
{
lock (configs)
{
configs.GetOrAddNew(typeof(T)).DiscriminatorProperty ??= discriminiatorProperty;
}
return this;
}
public string GetName<TBase, TDerived>() where TDerived : TBase where TBase : class
{
return GetName<TBase>(typeof(TDerived));
}
public string GetName<T>(Type derivedType) where T : class
{
if (!TryGetName<T>(derivedType, out var name))
{
ThrowHelper.ArgumentException($"Unknown derived type {derivedType}.", nameof(derivedType));
return default!;
}
return name;
}
public Type GetType<T>(string typeName) where T : class
{
if (!TryGetType<T>(typeName, out var name))
{
ThrowHelper.ArgumentException($"Unknown derived type {typeName}.", nameof(typeName));
return default!;
}
return name;
}
public bool TryGetName<T>(Type derivedType, [MaybeNullWhen(false)] out string typeName) where T : class
{
typeName = null!;
return TryGetConfig<T>(out var config) && config.TryGetName(derivedType, out typeName);
}
public bool TryGetType<T>(string typeName, [MaybeNullWhen(false)] out Type derivedType) where T : class
{
derivedType = null!;
return TryGetConfig<T>(out var config) && config.TryGetType(typeName, out derivedType);
}
public bool TryGetConfig<T>([MaybeNullWhen(false)] out TypeConfig config) where T : class
{
return TryGetConfig(typeof(T), out config);
}
public bool TryGetConfig(Type baseType, [MaybeNullWhen(false)] out TypeConfig config)
{
return configs.TryGetValue(baseType, out config);
}
}

34
backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
<LangVersion>10.0</LangVersion> <LangVersion>10.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<NeutralLanguage>en</NeutralLanguage> <NeutralLanguage>en</NeutralLanguage>
@ -11,33 +11,33 @@
<DebugSymbols>True</DebugSymbols> <DebugSymbols>True</DebugSymbols>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="MailKit" Version="3.2.0" /> <PackageReference Include="MailKit" Version="3.4.2" />
<PackageReference Include="McMaster.NETCore.Plugins" Version="1.4.0" /> <PackageReference Include="McMaster.NETCore.Plugins" Version="1.4.0" />
<PackageReference Include="Meziantou.Analyzer" Version="1.0.702"> <PackageReference Include="Meziantou.Analyzer" Version="1.0.746">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions" Version="6.0.5" /> <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.2.0" /> <PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.2.1" />
<PackageReference Include="Microsoft.OData.Core" Version="7.11.0" /> <PackageReference Include="Microsoft.OData.Core" Version="7.12.5" />
<PackageReference Include="NodaTime" Version="3.1.2" /> <PackageReference Include="NodaTime" Version="3.1.5" />
<PackageReference Include="OpenTelemetry.Api" Version="1.3.0" /> <PackageReference Include="OpenTelemetry.Api" Version="1.3.1" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.Assets" Version="4.13.0" /> <PackageReference Include="Squidex.Assets" Version="5.0.0" />
<PackageReference Include="Squidex.Caching" Version="4.13.0" /> <PackageReference Include="Squidex.Caching" Version="5.0.0" />
<PackageReference Include="Squidex.Hosting.Abstractions" Version="4.13.0" /> <PackageReference Include="Squidex.Hosting.Abstractions" Version="5.0.0" />
<PackageReference Include="Squidex.Log" Version="4.13.0" /> <PackageReference Include="Squidex.Log" Version="5.0.0" />
<PackageReference Include="Squidex.Messaging" Version="4.13.0" /> <PackageReference Include="Squidex.Messaging" Version="5.0.0" />
<PackageReference Include="Squidex.Text" Version="4.13.0" /> <PackageReference Include="Squidex.Text" Version="5.0.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="6.0.0" /> <PackageReference Include="System.Collections.Immutable" Version="7.0.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" /> <PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" /> <PackageReference Include="System.Linq.Async" Version="6.0.1" />
<PackageReference Include="System.Reactive" Version="5.0.0" /> <PackageReference Include="System.Reactive" Version="5.0.0" />
<PackageReference Include="System.Reflection.TypeExtensions" Version="4.7.0" /> <PackageReference Include="System.Reflection.TypeExtensions" Version="4.7.0" />
<PackageReference Include="System.Security.Claims" Version="4.3.0" /> <PackageReference Include="System.Security.Claims" Version="4.3.0" />
<PackageReference Include="System.Threading.Tasks.Dataflow" Version="6.0.0" /> <PackageReference Include="System.Threading.Tasks.Dataflow" Version="7.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<AdditionalFiles Include="..\..\stylecop.json" Link="stylecop.json" /> <AdditionalFiles Include="..\..\stylecop.json" Link="stylecop.json" />

4
backend/src/Squidex.Infrastructure/UsageTracking/CachingUsageTracker.cs

@ -67,7 +67,7 @@ public sealed class CachingUsageTracker : IUsageTracker
entry.AbsoluteExpirationRelativeToNow = CacheDuration; entry.AbsoluteExpirationRelativeToNow = CacheDuration;
return inner.GetForMonthAsync(key, date, category, ct); return inner.GetForMonthAsync(key, date, category, ct);
}); })!;
} }
public Task<Counters> GetAsync(string key, DateTime fromDate, DateTime toDate, string? category, public Task<Counters> GetAsync(string key, DateTime fromDate, DateTime toDate, string? category,
@ -82,6 +82,6 @@ public sealed class CachingUsageTracker : IUsageTracker
entry.AbsoluteExpirationRelativeToNow = CacheDuration; entry.AbsoluteExpirationRelativeToNow = CacheDuration;
return inner.GetAsync(key, fromDate, toDate, category, ct); return inner.GetAsync(key, fromDate, toDate, category, ct);
}); })!;
} }
} }

4
backend/src/Squidex.Shared/Squidex.Shared.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
<LangVersion>9.0</LangVersion> <LangVersion>9.0</LangVersion>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
@ -9,7 +9,7 @@
<DebugSymbols>True</DebugSymbols> <DebugSymbols>True</DebugSymbols>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="1.0.702"> <PackageReference Include="Meziantou.Analyzer" Version="1.0.746">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

9
backend/src/Squidex.Web/Extensions.cs

@ -8,7 +8,6 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Security.Claims; using System.Security.Claims;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Security;
namespace Squidex.Web; namespace Squidex.Web;
@ -62,11 +61,13 @@ public static class Extensions
{ {
result = null!; result = null!;
if (headers.TryGetValue(header, out var value) && value != StringValues.Empty) if (headers.TryGetValue(header, out var value))
{ {
if (!string.IsNullOrWhiteSpace(value)) string? valueString = value;
if (!string.IsNullOrWhiteSpace(valueString))
{ {
result = value; result = valueString;
return true; return true;
} }
} }

108
backend/src/Squidex.Web/Json/JsonInheritanceConverter.cs

@ -1,108 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Reflection;
using System.Runtime.Serialization;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.System;
#pragma warning disable RECS0108 // Warns about static fields in generic types
namespace Squidex.Web.Json;
public class JsonInheritanceConverter<T> : InheritanceConverterBase<T> where T : notnull
{
private static readonly Lazy<Dictionary<string, Type>> DefaultMapping = new Lazy<Dictionary<string, Type>>(() =>
{
var baseName = typeof(T).Name;
var result = new Dictionary<string, Type>();
void AddType(Type type)
{
var typeName = type.Name;
if (typeName.EndsWith(baseName, StringComparison.CurrentCulture))
{
typeName = typeName[..^baseName.Length];
}
result[typeName] = type;
}
foreach (var attribute in typeof(T).GetCustomAttributes<KnownTypeAttribute>())
{
if (attribute.Type != null)
{
if (!attribute.Type.IsAbstract)
{
AddType(attribute.Type);
}
}
else if (!string.IsNullOrWhiteSpace(attribute.MethodName))
{
var method = typeof(T).GetMethod(attribute.MethodName);
if (method != null && method.IsStatic)
{
var types = (IEnumerable<Type>)method.Invoke(null, Array.Empty<object>())!;
foreach (var type in types)
{
if (!type.IsAbstract)
{
AddType(type);
}
}
}
}
}
return result;
});
private readonly IReadOnlyDictionary<string, Type> mapping;
public JsonInheritanceConverter()
: this(null, DefaultMapping.Value)
{
}
public JsonInheritanceConverter(string? discriminatorName)
: this(discriminatorName, DefaultMapping.Value)
{
}
public JsonInheritanceConverter(string? discriminatorName, IReadOnlyDictionary<string, Type> mapping)
: base(GetDiscriminatorName(discriminatorName))
{
this.mapping = mapping ?? DefaultMapping.Value;
}
private static string GetDiscriminatorName(string? discriminatorName)
{
var attribute = typeof(T).GetCustomAttribute<JsonInheritanceConverterAttribute>();
return attribute?.DiscriminatorName ?? discriminatorName ?? "discriminator";
}
public override Type GetDiscriminatorType(string name, Type typeToConvert)
{
if (!mapping.TryGetValue(name, out var type))
{
ThrowHelper.JsonException($"Could not find subtype of '{typeToConvert.Name}' with discriminator '{name}'.");
return default!;
}
return type;
}
public override string GetDiscriminatorValue(Type type)
{
return mapping.FirstOrDefault(x => x.Value == type).Key ?? type.Name;
}
}

21
backend/src/Squidex.Web/Json/JsonInheritanceConverterAttribute.cs

@ -1,21 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Text.Json.Serialization;
namespace Squidex.Web.Json;
public sealed class JsonInheritanceConverterAttribute : JsonConverterAttribute
{
public string DiscriminatorName { get; }
public JsonInheritanceConverterAttribute(Type baseType, string discriminatorName = "$type")
: base(typeof(JsonInheritanceConverter<>).MakeGenericType(baseType))
{
DiscriminatorName = discriminatorName;
}
}

4
backend/src/Squidex.Web/Squidex.Web.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
<LangVersion>10.0</LangVersion> <LangVersion>10.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
@ -16,7 +16,7 @@
<PackageReference Include="GraphQL" Version="7.1.1" /> <PackageReference Include="GraphQL" Version="7.1.1" />
<PackageReference Include="GraphQL.SystemTextJson" Version="7.1.1" /> <PackageReference Include="GraphQL.SystemTextJson" Version="7.1.1" />
<PackageReference Include="GraphQL.Server.Transports.AspNetCore" Version="7.1.1" /> <PackageReference Include="GraphQL.Server.Transports.AspNetCore" Version="7.1.1" />
<PackageReference Include="Meziantou.Analyzer" Version="1.0.702"> <PackageReference Include="Meziantou.Analyzer" Version="1.0.746">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

15
backend/src/Squidex/Areas/Api/Config/OpenApi/CommonProcessor.cs

@ -16,14 +16,9 @@ namespace Squidex.Areas.Api.Config.OpenApi;
public sealed class CommonProcessor : IDocumentProcessor public sealed class CommonProcessor : IDocumentProcessor
{ {
private readonly string version; private readonly string version;
private readonly string logoBackground = "#3f83df"; private readonly string logoColor = "#3f83df";
private readonly string logoUrl; private readonly string logoUrl;
private readonly OpenApiExternalDocumentation documentation = new OpenApiExternalDocumentation
{
Url = "https://docs.squidex.io"
};
public CommonProcessor(ExposedValues exposedValues, IUrlGenerator urlGenerator) public CommonProcessor(ExposedValues exposedValues, IUrlGenerator urlGenerator)
{ {
logoUrl = urlGenerator.BuildUrl("images/logo-white.png", false); logoUrl = urlGenerator.BuildUrl("images/logo-white.png", false);
@ -36,6 +31,7 @@ public sealed class CommonProcessor : IDocumentProcessor
public void Process(DocumentProcessorContext context) public void Process(DocumentProcessorContext context)
{ {
context.Document.Info.Title = "Squidex API";
context.Document.Info.Version = version; context.Document.Info.Version = version;
context.Document.Info.ExtensionData = new Dictionary<string, object> context.Document.Info.ExtensionData = new Dictionary<string, object>
{ {
@ -43,10 +39,13 @@ public sealed class CommonProcessor : IDocumentProcessor
{ {
url = logoUrl, url = logoUrl,
backgroundStyle = string.Empty, backgroundStyle = string.Empty,
backgroundColor = logoBackground backgroundColor = logoColor
} }
}; };
context.Document.ExternalDocumentation = documentation; context.Document.ExternalDocumentation = new OpenApiExternalDocumentation
{
Url = "https://docs.squidex.io"
};
} }
} }

67
backend/src/Squidex/Areas/Api/Config/OpenApi/DiscriminatorProcessor.cs

@ -0,0 +1,67 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using NJsonSchema;
using NJsonSchema.Generation;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Config.OpenApi;
public sealed class DiscriminatorProcessor : ISchemaProcessor
{
private readonly TypeRegistry typeRegistry;
public DiscriminatorProcessor(TypeRegistry typeRegistry)
{
this.typeRegistry = typeRegistry;
}
public void Process(SchemaProcessorContext context)
{
if (context.Schema.DiscriminatorObject != null)
{
return;
}
if (!typeRegistry.TryGetConfig(context.ContextualType.Type, out var config) ||
config.DerivedTypes.Count <= 0 ||
config.DiscriminatorProperty == null)
{
return;
}
var discriminatorName = config.DiscriminatorProperty;
var discriminatorObject = new OpenApiDiscriminator
{
PropertyName = discriminatorName
};
var schema = context.Schema;
foreach (var (derivedType, typeName) in config.DerivedTypes)
{
var derivedSchema = context.Generator.Generate(derivedType, context.Resolver);
discriminatorObject.Mapping[typeName] = new JsonSchema
{
Reference = derivedSchema
};
}
schema.DiscriminatorObject = discriminatorObject;
if (!schema.Properties.TryGetValue(discriminatorName, out var existingProperty))
{
schema.Properties[discriminatorName] = existingProperty = new JsonSchemaProperty
{
Type = JsonObjectType.String
};
}
existingProperty.IsRequired = true;
}
}

69
backend/src/Squidex/Areas/Api/Config/OpenApi/ErrorDtoProcessor.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Xml.Linq;
using Namotion.Reflection; using Namotion.Reflection;
using NJsonSchema; using NJsonSchema;
using NSwag; using NSwag;
@ -14,54 +15,68 @@ using Squidex.Web;
namespace Squidex.Areas.Api.Config.OpenApi; namespace Squidex.Areas.Api.Config.OpenApi;
public sealed class ErrorDtoProcessor : IDocumentProcessor public sealed class ErrorDtoProcessor : IOperationProcessor
{ {
public void Process(DocumentProcessorContext context) public bool Process(OperationProcessorContext context)
{ {
var errorSchema = GetErrorSchema(context); var operation = context.OperationDescription.Operation;
foreach (var operation in context.Document.Paths.Values.SelectMany(x => x.Values)) void AddResponse(string code, string description)
{ {
AddErrorResponses(operation, errorSchema); if (!IsErrorCode(code))
{
return;
}
CleanupResponses(operation); if (!operation.Responses.ContainsKey(code))
{
operation.Responses.Add(code, new OpenApiResponse
{
Description = description
});
}
} }
}
private static void AddErrorResponses(OpenApiOperation operation, JsonSchema errorSchema) var responses =
{ context.MethodInfo.GetXmlDocsElement(null)?
if (!operation.Responses.ContainsKey("500")) .Nodes()
{ .OfType<XElement>()
const string description = "Operation failed."; .Where(x => x.Name == "response")
.Where(x => x.Attribute("code") != null)
?? Enumerable.Empty<XElement>();
var response = new OpenApiResponse { Description = description, Schema = errorSchema }; foreach (var response in responses)
{
AddResponse(response.Attribute("code")!.Value, response.Value);
}
operation.Responses["500"] = response; if (!string.Equals(context.OperationDescription.Method, HttpMethods.Get, StringComparison.OrdinalIgnoreCase))
{
AddResponse("400", "Validation error.");
} }
AddResponse("500", "Operation failed.");
foreach (var (code, response) in operation.Responses) foreach (var (code, response) in operation.Responses)
{ {
if (code != "404" && code.StartsWith("4", StringComparison.OrdinalIgnoreCase) && response.Schema == null) if (response.Schema == null)
{ {
response.Schema = errorSchema; if (IsErrorCode(code) && code != "404")
{
response.Schema = GetErrorSchema(context);
}
} }
} }
return true;
} }
private static void CleanupResponses(OpenApiOperation operation) private static bool IsErrorCode(string code)
{ {
foreach (var (code, response) in operation.Responses.ToList()) return !code.StartsWith("2", StringComparison.OrdinalIgnoreCase);
{
if (string.IsNullOrWhiteSpace(response.Description) ||
response.Description?.Contains("=&gt;", StringComparison.Ordinal) == true ||
response.Description?.Contains("=>", StringComparison.Ordinal) == true)
{
operation.Responses.Remove(code);
}
}
} }
private static JsonSchema GetErrorSchema(DocumentProcessorContext context) private static JsonSchema GetErrorSchema(OperationProcessorContext context)
{ {
var errorType = typeof(ErrorDto).ToContextualType(); var errorType = typeof(ErrorDto).ToContextualType();

30
backend/src/Squidex/Areas/Api/Config/OpenApi/FixProcessor.cs

@ -1,30 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using NJsonSchema;
using NSwag.Generation.Processors;
using NSwag.Generation.Processors.Contexts;
namespace Squidex.Areas.Api.Config.OpenApi;
public sealed class FixProcessor : IOperationProcessor
{
private static readonly JsonSchema StringSchema = new JsonSchema { Type = JsonObjectType.String };
public bool Process(OperationProcessorContext context)
{
foreach (var (_, parameter) in context.Parameters)
{
if (parameter.IsRequired && parameter.Schema is { Type: JsonObjectType.String })
{
parameter.Schema = StringSchema;
}
}
return true;
}
}

77
backend/src/Squidex/Areas/Api/Config/OpenApi/OpenApiServices.cs

@ -12,13 +12,13 @@ using NJsonSchema.Generation.TypeMappers;
using NodaTime; using NodaTime;
using NSwag.Generation; using NSwag.Generation;
using NSwag.Generation.Processors; using NSwag.Generation.Processors;
using Squidex.Areas.Api.Controllers.Rules.Models;
using Squidex.Domain.Apps.Core.Assets; using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Queries; using Squidex.Infrastructure.Queries;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Config.OpenApi; namespace Squidex.Areas.Api.Config.OpenApi;
@ -27,15 +27,12 @@ public static class OpenApiServices
public static void AddSquidexOpenApiSettings(this IServiceCollection services) public static void AddSquidexOpenApiSettings(this IServiceCollection services)
{ {
services.AddSingletonAs<ErrorDtoProcessor>() services.AddSingletonAs<ErrorDtoProcessor>()
.As<IDocumentProcessor>(); .As<IOperationProcessor>();
services.AddSingletonAs<RuleActionProcessor>()
.As<IDocumentProcessor>();
services.AddSingletonAs<CommonProcessor>() services.AddSingletonAs<CommonProcessor>()
.As<IDocumentProcessor>(); .As<IDocumentProcessor>();
services.AddSingletonAs<XmlTagProcessor>() services.AddSingletonAs<TagXmlProcessor>()
.As<IDocumentProcessor>(); .As<IDocumentProcessor>();
services.AddSingletonAs<SecurityProcessor>() services.AddSingletonAs<SecurityProcessor>()
@ -44,14 +41,11 @@ public static class OpenApiServices
services.AddSingletonAs<ScopesProcessor>() services.AddSingletonAs<ScopesProcessor>()
.As<IOperationProcessor>(); .As<IOperationProcessor>();
services.AddSingletonAs<FixProcessor>()
.As<IOperationProcessor>();
services.AddSingletonAs<TagByGroupNameProcessor>() services.AddSingletonAs<TagByGroupNameProcessor>()
.As<IOperationProcessor>(); .As<IOperationProcessor>();
services.AddSingletonAs<XmlResponseTypesProcessor>() services.AddSingletonAs<SchemaNameGenerator>()
.As<IOperationProcessor>(); .As<ISchemaNameGenerator>();
services.AddSingletonAs<JsonSchemaGenerator>() services.AddSingletonAs<JsonSchemaGenerator>()
.AsSelf(); .AsSelf();
@ -63,7 +57,7 @@ public static class OpenApiServices
{ {
var settings = new JsonSchemaGeneratorSettings(); var settings = new JsonSchemaGeneratorSettings();
ConfigureSchemaSettings(settings, true); ConfigureSchemaSettings(settings, c.GetRequiredService<TypeRegistry>(), true);
return settings; return settings;
}); });
@ -72,7 +66,7 @@ public static class OpenApiServices
{ {
var settings = new OpenApiDocumentGeneratorSettings(); var settings = new OpenApiDocumentGeneratorSettings();
ConfigureSchemaSettings(settings, true); ConfigureSchemaSettings(settings, c.GetRequiredService<TypeRegistry>(), true);
foreach (var processor in c.GetRequiredService<IEnumerable<IDocumentProcessor>>()) foreach (var processor in c.GetRequiredService<IEnumerable<IDocumentProcessor>>())
{ {
@ -82,56 +76,42 @@ public static class OpenApiServices
return settings; return settings;
}); });
services.AddOpenApiDocument(settings => services.AddOpenApiDocument((settings, services) =>
{ {
settings.Title = "Squidex API"; ConfigureSchemaSettings(settings, services.GetRequiredService<TypeRegistry>(), false);
ConfigureSchemaSettings(settings);
settings.OperationProcessors.Add(new QueryParamsProcessor("/api/apps/{app}/assets")); settings.OperationProcessors.Add(new QueryParamsProcessor("/api/apps/{app}/assets"));
}); });
} }
private static void ConfigureSchemaSettings(JsonSchemaGeneratorSettings settings, bool flatten = false) private static void ConfigureSchemaSettings(JsonSchemaGeneratorSettings settings, TypeRegistry typeRegistry, bool flatten)
{ {
settings.AllowReferencesWithProperties = true;
settings.ReflectionService = new ReflectionServices();
settings.TypeMappers = new List<ITypeMapper> settings.TypeMappers = new List<ITypeMapper>
{ {
CreateAnyMap<FilterNode<JsonValue>>(),
CreateAnyMap<JsonDocument>(),
CreateAnyMap<JsonValue>(),
CreateArrayMap<FieldNames>(JsonObjectType.String),
CreateObjectMap<AssetMetadata>(),
CreateObjectMap<JsonObject>(),
CreateStringMap<DomainId>(), CreateStringMap<DomainId>(),
CreateStringMap<Instant>(JsonFormatStrings.DateTime), CreateStringMap<Instant>(JsonFormatStrings.DateTime),
CreateStringMap<Language>(),
CreateStringMap<LocalDate>(JsonFormatStrings.Date), CreateStringMap<LocalDate>(JsonFormatStrings.Date),
CreateStringMap<LocalDateTime>(JsonFormatStrings.DateTime), CreateStringMap<LocalDateTime>(JsonFormatStrings.DateTime),
CreateStringMap<Language>(),
CreateStringMap<NamedId<DomainId>>(), CreateStringMap<NamedId<DomainId>>(),
CreateStringMap<NamedId<Guid>>(), CreateStringMap<NamedId<Guid>>(),
CreateStringMap<NamedId<string>>(), CreateStringMap<NamedId<string>>(),
CreateStringMap<RefToken>(), CreateStringMap<RefToken>(),
CreateStringMap<Status>(), CreateStringMap<Status>(),
CreateObjectMap<JsonObject>(),
CreateObjectMap<AssetMetadata>(),
CreateAnyMap<JsonDocument>(),
CreateAnyMap<JsonValue>(),
CreateAnyMap<FilterNode<JsonValue>>(),
new PrimitiveTypeMapper(typeof(FieldNames), schema =>
{
schema.Type = JsonObjectType.Array;
schema.Item = new JsonSchema
{
Type = JsonObjectType.String
};
}),
}; };
settings.SchemaType = NJsonSchema.SchemaType.OpenApi3; settings.AllowReferencesWithProperties = true;
settings.FlattenInheritanceHierarchy = flatten; settings.FlattenInheritanceHierarchy = flatten;
settings.SchemaNameGenerator = new SchemaNameGenerator();
settings.SchemaProcessors.Add(new DiscriminatorProcessor(typeRegistry));
settings.SchemaType = NJsonSchema.SchemaType.OpenApi3;
settings.ReflectionService = new ReflectionServices();
} }
private static ITypeMapper CreateObjectMap<T>() private static ITypeMapper CreateObjectMap<T>()
@ -147,6 +127,19 @@ public static class OpenApiServices
}); });
} }
private static ITypeMapper CreateArrayMap<T>(JsonObjectType itemType)
{
return new PrimitiveTypeMapper(typeof(T), schema =>
{
schema.Type = JsonObjectType.Array;
schema.Item = new JsonSchema
{
Type = itemType
};
});
}
private static ITypeMapper CreateStringMap<T>(string? format = null) private static ITypeMapper CreateStringMap<T>(string? format = null)
{ {
return new PrimitiveTypeMapper(typeof(T), schema => return new PrimitiveTypeMapper(typeof(T), schema =>

30
backend/src/Squidex/Areas/Api/Config/OpenApi/SchemaNameGenerator.cs

@ -0,0 +1,30 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using NJsonSchema.Generation;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Config.OpenApi;
public sealed class SchemaNameGenerator : DefaultSchemaNameGenerator
{
public override string Generate(Type type)
{
if (type.BaseType == typeof(RuleAction))
{
return $"{type.TypeName(false, "Action")}RuleActionDto";
}
if (type == typeof(RuleAction))
{
return $"RuleActionDto";
}
return base.Generate(type);
}
}

41
backend/src/Squidex/Areas/Api/Config/OpenApi/SecurityProcessor.cs

@ -15,45 +15,28 @@ namespace Squidex.Areas.Api.Config.OpenApi;
public sealed class SecurityProcessor : SecurityDefinitionAppender public sealed class SecurityProcessor : SecurityDefinitionAppender
{ {
public SecurityProcessor(IUrlGenerator urlGenerator) public SecurityProcessor(IUrlGenerator urlGenerator)
: base(Constants.SecurityDefinition, Enumerable.Empty<string>(), CreateOAuthSchema(urlGenerator)) : base(Constants.SecurityDefinition, new[] { Constants.ScopeApi }, CreateOAuthSchema(urlGenerator))
{ {
} }
private static OpenApiSecurityScheme CreateOAuthSchema(IUrlGenerator urlGenerator) private static OpenApiSecurityScheme CreateOAuthSchema(IUrlGenerator urlGenerator)
{ {
var security = new OpenApiSecurityScheme string BuildUrl(string endpoint)
{ {
Type = OpenApiSecuritySchemeType.OAuth2 return urlGenerator.BuildUrl($"/{Constants.PrefixIdentityServer}/{endpoint}", false);
}; }
var tokenUrl = urlGenerator.BuildUrl($"/{Constants.PrefixIdentityServer}/connect/token", false);
security.TokenUrl = tokenUrl; var security = new OpenApiSecurityScheme
{
SetupDescription(security, tokenUrl); Type = OpenApiSecuritySchemeType.OpenIdConnect,
SetupFlow(security);
SetupScopes(security);
return security; // The discovery endpoint
} OpenIdConnectUrl = BuildUrl(".well-known/openid-configuration"),
private static void SetupFlow(OpenApiSecurityScheme security) // Just described the token URL again.
{ Description = Properties.Resources.OpenApiSecurity.Replace("<TOKEN_URL>", BuildUrl($"connect/token"), StringComparison.Ordinal)
security.Flow = OpenApiOAuth2Flow.Application;
}
private static void SetupScopes(OpenApiSecurityScheme security)
{
security.Scopes = new Dictionary<string, string>
{
[Constants.ScopeApi] = "Read and write access to the API"
}; };
}
private static void SetupDescription(OpenApiSecurityScheme securityScheme, string tokenUrl)
{
var securityText = Properties.Resources.OpenApiSecurity.Replace("<TOKEN_URL>", tokenUrl, StringComparison.Ordinal);
securityScheme.Description = securityText; return security;
} }
} }

2
backend/src/Squidex/Areas/Api/Config/OpenApi/XmlTagProcessor.cs → backend/src/Squidex/Areas/Api/Config/OpenApi/TagXmlProcessor.cs

@ -13,7 +13,7 @@ using NSwag.Generation.Processors.Contexts;
namespace Squidex.Areas.Api.Config.OpenApi; namespace Squidex.Areas.Api.Config.OpenApi;
public sealed class XmlTagProcessor : IDocumentProcessor public sealed class TagXmlProcessor : IDocumentProcessor
{ {
public void Process(DocumentProcessorContext context) public void Process(DocumentProcessorContext context)
{ {

54
backend/src/Squidex/Areas/Api/Config/OpenApi/XmlResponseTypesProcessor.cs

@ -1,54 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Text.RegularExpressions;
using Namotion.Reflection;
using NSwag;
using NSwag.Generation.Processors;
using NSwag.Generation.Processors.Contexts;
using Squidex.Infrastructure;
namespace Squidex.Areas.Api.Config.OpenApi;
public sealed class XmlResponseTypesProcessor : IOperationProcessor
{
private static readonly Regex ResponseRegex = new Regex("(?<Code>[0-9]{3})[\\s]*=((&gt;)|>)[\\s]*(?<Description>.*)", RegexOptions.Compiled | RegexOptions.ExplicitCapture);
public bool Process(OperationProcessorContext context)
{
var operation = context.OperationDescription.Operation;
var returnsDescription = context.MethodInfo.GetXmlDocsTag("returns");
if (!string.IsNullOrWhiteSpace(returnsDescription))
{
foreach (var match in ResponseRegex.Matches(returnsDescription).OfType<Match>())
{
var statusCode = match.Groups["Code"].Value;
if (!operation.Responses.TryGetValue(statusCode, out var response))
{
response = new OpenApiResponse();
operation.Responses[statusCode] = response;
}
var description = match.Groups["Description"].Value;
if (description.Contains("=&gt;", StringComparison.Ordinal))
{
ThrowHelper.InvalidOperationException("Description not formatted correcly.");
return default!;
}
response.Description = description;
}
}
return true;
}
}

14
backend/src/Squidex/Areas/Api/Controllers/Apps/AppAssetsController.cs

@ -29,10 +29,8 @@ public sealed class AppAssetsController : ApiController
/// Get the app asset scripts. /// Get the app asset scripts.
/// </summary> /// </summary>
/// <param name="app">The name of the app to get the asset scripts for.</param> /// <param name="app">The name of the app to get the asset scripts for.</param>
/// <returns> /// <response code="200">Asset scripts returned.</response>.
/// 200 => Asset scripts returned. /// <response code="404">App not found.</response>.
/// 404 => App not found.
/// </returns>
[HttpGet] [HttpGet]
[Route("apps/{app}/assets/scripts")] [Route("apps/{app}/assets/scripts")]
[ProducesResponseType(typeof(AssetScriptsDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(AssetScriptsDto), StatusCodes.Status200OK)]
@ -53,11 +51,9 @@ public sealed class AppAssetsController : ApiController
/// </summary> /// </summary>
/// <param name="app">The name of the app to update.</param> /// <param name="app">The name of the app to update.</param>
/// <param name="request">The values to update.</param> /// <param name="request">The values to update.</param>
/// <returns> /// <response code="200">Asset scripts updated.</response>.
/// 200 => Asset scripts updated. /// <response code="400">Asset request not valid.</response>.
/// 400 => Asset request not valid. /// <response code="404">App not found.</response>.
/// 404 => App not found.
/// </returns>
[HttpPut] [HttpPut]
[Route("apps/{app}/assets/scripts")] [Route("apps/{app}/assets/scripts")]
[ProducesResponseType(typeof(AssetScriptsDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(AssetScriptsDto), StatusCodes.Status200OK)]

28
backend/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs

@ -31,10 +31,8 @@ public sealed class AppClientsController : ApiController
/// Get app clients. /// Get app clients.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <returns> /// <response code="200">Clients returned.</response>.
/// 200 => Clients returned. /// <response code="404">App not found.</response>.
/// 404 => App not found.
/// </returns>
/// <remarks> /// <remarks>
/// Gets all configured clients for the app with the specified name. /// Gets all configured clients for the app with the specified name.
/// </remarks> /// </remarks>
@ -60,11 +58,9 @@ public sealed class AppClientsController : ApiController
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="request">Client object that needs to be added to the app.</param> /// <param name="request">Client object that needs to be added to the app.</param>
/// <returns> /// <response code="201">Client created.</response>.
/// 201 => Client created. /// <response code="400">Client request not valid.</response>.
/// 400 => Client request not valid. /// <response code="404">App not found.</response>.
/// 404 => App not found.
/// </returns>
/// <remarks> /// <remarks>
/// Create a new client for the app with the specified name. /// Create a new client for the app with the specified name.
/// The client secret is auto generated on the server and returned. The client does not expire, the access token is valid for 30 days. /// The client secret is auto generated on the server and returned. The client does not expire, the access token is valid for 30 days.
@ -89,11 +85,9 @@ public sealed class AppClientsController : ApiController
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="id">The ID of the client that must be updated.</param> /// <param name="id">The ID of the client that must be updated.</param>
/// <param name="request">Client object that needs to be updated.</param> /// <param name="request">Client object that needs to be updated.</param>
/// <returns> /// <response code="200">Client updated.</response>.
/// 200 => Client updated. /// <response code="400">Client request not valid.</response>.
/// 400 => Client request not valid. /// <response code="404">Client or app not found.</response>.
/// 404 => Client or app not found.
/// </returns>
/// <remarks> /// <remarks>
/// Only the display name can be changed, create a new client if necessary. /// Only the display name can be changed, create a new client if necessary.
/// </remarks> /// </remarks>
@ -116,10 +110,8 @@ public sealed class AppClientsController : ApiController
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="id">The ID of the client that must be deleted.</param> /// <param name="id">The ID of the client that must be deleted.</param>
/// <returns> /// <response code="200">Client deleted.</response>.
/// 200 => Client deleted. /// <response code="404">Client or app not found.</response>.
/// 404 => Client or app not found.
/// </returns>
/// <remarks> /// <remarks>
/// The application that uses this client credentials cannot access the API after it has been revoked. /// The application that uses this client credentials cannot access the API after it has been revoked.
/// </remarks> /// </remarks>

26
backend/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs

@ -38,10 +38,8 @@ public sealed class AppContributorsController : ApiController
/// Get app contributors. /// Get app contributors.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <returns> /// <response code="200">Contributors returned.</response>.
/// 200 => Contributors returned. /// <response code="404">App not found.</response>.
/// 404 => App not found.
/// </returns>
[HttpGet] [HttpGet]
[Route("apps/{app}/contributors/")] [Route("apps/{app}/contributors/")]
[ProducesResponseType(typeof(ContributorsDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ContributorsDto), StatusCodes.Status200OK)]
@ -59,11 +57,9 @@ public sealed class AppContributorsController : ApiController
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="request">Contributor object that needs to be added to the app.</param> /// <param name="request">Contributor object that needs to be added to the app.</param>
/// <returns> /// <response code="201">Contributor assigned to app.</response>.
/// 201 => Contributor assigned to app. /// <response code="400">Contributor request not valid.</response>.
/// 400 => Contributor request not valid. /// <response code="404">App not found.</response>.
/// 404 => App not found.
/// </returns>
[HttpPost] [HttpPost]
[Route("apps/{app}/contributors/")] [Route("apps/{app}/contributors/")]
[ProducesResponseType(typeof(ContributorsDto), StatusCodes.Status201Created)] [ProducesResponseType(typeof(ContributorsDto), StatusCodes.Status201Created)]
@ -82,10 +78,8 @@ public sealed class AppContributorsController : ApiController
/// Remove yourself. /// Remove yourself.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <returns> /// <response code="200">Contributor removed.</response>.
/// 200 => Contributor removed. /// <response code="404">Contributor or app not found.</response>.
/// 404 => Contributor or app not found.
/// </returns>
[HttpDelete] [HttpDelete]
[Route("apps/{app}/contributors/me/")] [Route("apps/{app}/contributors/me/")]
[ProducesResponseType(typeof(ContributorsDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ContributorsDto), StatusCodes.Status200OK)]
@ -105,10 +99,8 @@ public sealed class AppContributorsController : ApiController
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="id">The ID of the contributor.</param> /// <param name="id">The ID of the contributor.</param>
/// <returns> /// <response code="200">Contributor removed.</response>.
/// 200 => Contributor removed. /// <response code="404">Contributor or app not found.</response>.
/// 404 => Contributor or app not found.
/// </returns>
[HttpDelete] [HttpDelete]
[Route("apps/{app}/contributors/{id}/")] [Route("apps/{app}/contributors/{id}/")]
[ProducesResponseType(typeof(ContributorsDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ContributorsDto), StatusCodes.Status200OK)]

6
backend/src/Squidex/Areas/Api/Controllers/Apps/AppImageController.cs

@ -41,10 +41,8 @@ public sealed class AppImageController : ApiController
/// Get the app image. /// Get the app image.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <returns> /// <response code="200">App image found and content or (resized) image returned.</response>.
/// 200 => App image found and content or (resized) image returned. /// <response code="404">App not found.</response>.
/// 404 => App not found.
/// </returns>
[HttpGet] [HttpGet]
[Route("apps/{app}/image")] [Route("apps/{app}/image")]
[ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)] [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)]

30
backend/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs

@ -32,10 +32,8 @@ public sealed class AppLanguagesController : ApiController
/// Get app languages. /// Get app languages.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <returns> /// <response code="200">Languages returned.</response>.
/// 200 => Languages returned. /// <response code="404">App not found.</response>.
/// 404 => App not found.
/// </returns>
[HttpGet] [HttpGet]
[Route("apps/{app}/languages/")] [Route("apps/{app}/languages/")]
[ProducesResponseType(typeof(AppLanguagesDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(AppLanguagesDto), StatusCodes.Status200OK)]
@ -58,11 +56,9 @@ public sealed class AppLanguagesController : ApiController
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="request">The language to add to the app.</param> /// <param name="request">The language to add to the app.</param>
/// <returns> /// <response code="201">Language created.</response>.
/// 201 => Language created. /// <response code="400">Language request not valid.</response>.
/// 400 => Language request not valid. /// <response code="404">App not found.</response>.
/// 404 => App not found.
/// </returns>
[HttpPost] [HttpPost]
[Route("apps/{app}/languages/")] [Route("apps/{app}/languages/")]
[ProducesResponseType(typeof(AppLanguagesDto), 201)] [ProducesResponseType(typeof(AppLanguagesDto), 201)]
@ -83,11 +79,9 @@ public sealed class AppLanguagesController : ApiController
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="language">The language to update.</param> /// <param name="language">The language to update.</param>
/// <param name="request">The language object.</param> /// <param name="request">The language object.</param>
/// <returns> /// <response code="200">Language updated.</response>.
/// 200 => Language updated. /// <response code="400">Language request not valid.</response>.
/// 400 => Language request not valid. /// <response code="404">Language or app not found.</response>.
/// 404 => Language or app not found.
/// </returns>
[HttpPut] [HttpPut]
[Route("apps/{app}/languages/{language}/")] [Route("apps/{app}/languages/{language}/")]
[ProducesResponseType(typeof(AppLanguagesDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(AppLanguagesDto), StatusCodes.Status200OK)]
@ -107,11 +101,9 @@ public sealed class AppLanguagesController : ApiController
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="language">The language to delete from the app.</param> /// <param name="language">The language to delete from the app.</param>
/// <returns> /// <response code="200">Language deleted.</response>.
/// 200 => Language deleted. /// <response code="400">Language is master language.</response>.
/// 400 => Language is master language. /// <response code="404">Language or app not found.</response>.
/// 404 => Language or app not found.
/// </returns>
[HttpDelete] [HttpDelete]
[Route("apps/{app}/languages/{language}/")] [Route("apps/{app}/languages/{language}/")]
[ProducesResponseType(typeof(AppLanguagesDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(AppLanguagesDto), StatusCodes.Status200OK)]

36
backend/src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs

@ -35,10 +35,8 @@ public sealed class AppRolesController : ApiController
/// Get app roles. /// Get app roles.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <returns> /// <response code="200">Roles returned.</response>.
/// 200 => Roles returned. /// <response code="404">App not found.</response>.
/// 404 => App not found.
/// </returns>
[HttpGet] [HttpGet]
[Route("apps/{app}/roles/")] [Route("apps/{app}/roles/")]
[ProducesResponseType(typeof(RolesDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(RolesDto), StatusCodes.Status200OK)]
@ -60,10 +58,8 @@ public sealed class AppRolesController : ApiController
/// Get app permissions. /// Get app permissions.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <returns> /// <response code="200">App permissions returned.</response>.
/// 200 => App permissions returned. /// <response code="404">App not found.</response>.
/// 404 => App not found.
/// </returns>
[HttpGet] [HttpGet]
[Route("apps/{app}/roles/permissions")] [Route("apps/{app}/roles/permissions")]
[ProducesResponseType(typeof(string[]), StatusCodes.Status200OK)] [ProducesResponseType(typeof(string[]), StatusCodes.Status200OK)]
@ -86,11 +82,9 @@ public sealed class AppRolesController : ApiController
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="request">Role object that needs to be added to the app.</param> /// <param name="request">Role object that needs to be added to the app.</param>
/// <returns> /// <response code="201">Role created.</response>.
/// 201 => Role created. /// <response code="400">Role request not valid.</response>.
/// 400 => Role request not valid. /// <response code="404">App not found.</response>.
/// 404 => App not found.
/// </returns>
[HttpPost] [HttpPost]
[Route("apps/{app}/roles/")] [Route("apps/{app}/roles/")]
[ProducesResponseType(typeof(RolesDto), 201)] [ProducesResponseType(typeof(RolesDto), 201)]
@ -111,11 +105,9 @@ public sealed class AppRolesController : ApiController
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="roleName">The name of the role to be updated.</param> /// <param name="roleName">The name of the role to be updated.</param>
/// <param name="request">Role to be updated for the app.</param> /// <param name="request">Role to be updated for the app.</param>
/// <returns> /// <response code="200">Role updated.</response>.
/// 200 => Role updated. /// <response code="400">Role request not valid.</response>.
/// 400 => Role request not valid. /// <response code="404">Role or app not found.</response>.
/// 404 => Role or app not found.
/// </returns>
[HttpPut] [HttpPut]
[Route("apps/{app}/roles/{roleName}/")] [Route("apps/{app}/roles/{roleName}/")]
[ProducesResponseType(typeof(RolesDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(RolesDto), StatusCodes.Status200OK)]
@ -136,11 +128,9 @@ public sealed class AppRolesController : ApiController
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="roleName">The name of the role.</param> /// <param name="roleName">The name of the role.</param>
/// <returns> /// <response code="200">Role deleted.</response>.
/// 200 => Role deleted. /// <response code="400">Role is in use by contributor or client or a default role.</response>.
/// 400 => Role is in use by contributor or client or a default role. /// <response code="404">Role or app not found.</response>.
/// 404 => Role or app not found.
/// </returns>
[HttpDelete] [HttpDelete]
[Route("apps/{app}/roles/{roleName}/")] [Route("apps/{app}/roles/{roleName}/")]
[ProducesResponseType(typeof(RolesDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(RolesDto), StatusCodes.Status200OK)]

14
backend/src/Squidex/Areas/Api/Controllers/Apps/AppSettingsController.cs

@ -29,10 +29,8 @@ public sealed class AppSettingsController : ApiController
/// Get the app settings. /// Get the app settings.
/// </summary> /// </summary>
/// <param name="app">The name of the app to get the settings for.</param> /// <param name="app">The name of the app to get the settings for.</param>
/// <returns> /// <response code="200">App settings returned.</response>.
/// 200 => App settings returned. /// <response code="404">App not found.</response>.
/// 404 => App not found.
/// </returns>
[HttpGet] [HttpGet]
[Route("apps/{app}/settings")] [Route("apps/{app}/settings")]
[ProducesResponseType(typeof(AppSettingsDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(AppSettingsDto), StatusCodes.Status200OK)]
@ -53,11 +51,9 @@ public sealed class AppSettingsController : ApiController
/// </summary> /// </summary>
/// <param name="app">The name of the app to update.</param> /// <param name="app">The name of the app to update.</param>
/// <param name="request">The values to update.</param> /// <param name="request">The values to update.</param>
/// <returns> /// <response code="200">App updated.</response>.
/// 200 => App updated. /// <response code="400">App request not valid.</response>.
/// 400 => App request not valid. /// <response code="404">App not found.</response>.
/// 404 => App not found.
/// </returns>
[HttpPut] [HttpPut]
[Route("apps/{app}/settings")] [Route("apps/{app}/settings")]
[ProducesResponseType(typeof(AppSettingsDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(AppSettingsDto), StatusCodes.Status200OK)]

28
backend/src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs

@ -36,10 +36,8 @@ public sealed class AppWorkflowsController : ApiController
/// Get app workflow. /// Get app workflow.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <returns> /// <response code="200">Workflows returned.</response>.
/// 200 => Workflows returned. /// <response code="404">App not found.</response>.
/// 404 => App not found.
/// </returns>
[HttpGet] [HttpGet]
[Route("apps/{app}/workflows/")] [Route("apps/{app}/workflows/")]
[ProducesResponseType(typeof(WorkflowsDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(WorkflowsDto), StatusCodes.Status200OK)]
@ -62,11 +60,9 @@ public sealed class AppWorkflowsController : ApiController
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="request">The new workflow.</param> /// <param name="request">The new workflow.</param>
/// <returns> /// <response code="200">Workflow created.</response>.
/// 200 => Workflow created. /// <response code="400">Workflow request not valid.</response>.
/// 400 => Workflow request not valid. /// <response code="404">Workflow or app not found.</response>.
/// 404 => Workflow or app not found.
/// </returns>
[HttpPost] [HttpPost]
[Route("apps/{app}/workflows/")] [Route("apps/{app}/workflows/")]
[ProducesResponseType(typeof(WorkflowsDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(WorkflowsDto), StatusCodes.Status200OK)]
@ -87,11 +83,9 @@ public sealed class AppWorkflowsController : ApiController
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="id">The ID of the workflow to update.</param> /// <param name="id">The ID of the workflow to update.</param>
/// <param name="request">The new workflow.</param> /// <param name="request">The new workflow.</param>
/// <returns> /// <response code="200">Workflow updated.</response>.
/// 200 => Workflow updated. /// <response code="400">Workflow request not valid.</response>.
/// 400 => Workflow request not valid. /// <response code="404">Workflow or app not found.</response>.
/// 404 => Workflow or app not found.
/// </returns>
[HttpPut] [HttpPut]
[Route("apps/{app}/workflows/{id}")] [Route("apps/{app}/workflows/{id}")]
[ProducesResponseType(typeof(WorkflowsDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(WorkflowsDto), StatusCodes.Status200OK)]
@ -111,10 +105,8 @@ public sealed class AppWorkflowsController : ApiController
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="id">The ID of the workflow to update.</param> /// <param name="id">The ID of the workflow to update.</param>
/// <returns> /// <response code="200">Workflow deleted.</response>.
/// 200 => Workflow deleted. /// <response code="404">Workflow or app not found.</response>.
/// 404 => Workflow or app not found.
/// </returns>
[HttpDelete] [HttpDelete]
[Route("apps/{app}/workflows/{id}")] [Route("apps/{app}/workflows/{id}")]
[ProducesResponseType(typeof(WorkflowsDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(WorkflowsDto), StatusCodes.Status200OK)]

59
backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs

@ -37,9 +37,7 @@ public sealed class AppsController : ApiController
/// <summary> /// <summary>
/// Get your apps. /// Get your apps.
/// </summary> /// </summary>
/// <returns> /// <response code="200">Apps returned.</response>.
/// 200 => Apps returned.
/// </returns>
/// <remarks> /// <remarks>
/// You can only retrieve the list of apps when you are authenticated as a user (OpenID implicit flow). /// 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. /// You will retrieve all apps, where you are assigned as a contributor.
@ -70,9 +68,7 @@ public sealed class AppsController : ApiController
/// Get team apps. /// Get team apps.
/// </summary> /// </summary>
/// <param name="team">The ID of the team.</param> /// <param name="team">The ID of the team.</param>
/// <returns> /// <response code="200">Apps returned.</response>.
/// 200 => Apps returned.
/// </returns>
/// <remarks> /// <remarks>
/// You can only retrieve the list of apps when you are authenticated as a user (OpenID implicit flow). /// 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. /// You will retrieve all apps, where you are assigned as a contributor.
@ -100,10 +96,8 @@ public sealed class AppsController : ApiController
/// Get an app by name. /// Get an app by name.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <returns> /// <response code="200">Apps returned.</response>.
/// 200 => Apps returned. /// <response code="404">App not found.</response>.
/// 404 => App not found.
/// </returns>
[HttpGet] [HttpGet]
[Route("apps/{app}")] [Route("apps/{app}")]
[ProducesResponseType(typeof(AppDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(AppDto), StatusCodes.Status200OK)]
@ -127,11 +121,9 @@ public sealed class AppsController : ApiController
/// Create a new app. /// Create a new app.
/// </summary> /// </summary>
/// <param name="request">The app object that needs to be added to Squidex.</param> /// <param name="request">The app object that needs to be added to Squidex.</param>
/// <returns> /// <response code="201">App created.</response>.
/// 201 => App created. /// <response code="400">App request not valid.</response>.
/// 400 => App request not valid. /// <response code="409">App name is already in use.</response>.
/// 409 => App name is already in use.
/// </returns>
/// <remarks> /// <remarks>
/// You can only create an app when you are authenticated as a user (OpenID implicit flow). /// 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. /// You will be assigned as owner of the new app automatically.
@ -153,11 +145,9 @@ public sealed class AppsController : ApiController
/// </summary> /// </summary>
/// <param name="app">The name of the app to update.</param> /// <param name="app">The name of the app to update.</param>
/// <param name="request">The values to update.</param> /// <param name="request">The values to update.</param>
/// <returns> /// <response code="200">App updated.</response>.
/// 200 => App updated. /// <response code="400">App request not valid.</response>.
/// 400 => App request not valid. /// <response code="404">App not found.</response>.
/// 404 => App not found.
/// </returns>
[HttpPut] [HttpPut]
[Route("apps/{app}/")] [Route("apps/{app}/")]
[ProducesResponseType(typeof(AppDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(AppDto), StatusCodes.Status200OK)]
@ -175,11 +165,9 @@ public sealed class AppsController : ApiController
/// </summary> /// </summary>
/// <param name="app">The name of the app to update.</param> /// <param name="app">The name of the app to update.</param>
/// <param name="request">The team information.</param> /// <param name="request">The team information.</param>
/// <returns> /// <response code="200">App transferred.</response>.
/// 200 => App transferred. /// <response code="400">App request not valid.</response>.
/// 400 => App request not valid. /// <response code="404">App not found.</response>.
/// 404 => App not found.
/// </returns>
[HttpPut] [HttpPut]
[Route("apps/{app}/team")] [Route("apps/{app}/team")]
[ProducesResponseType(typeof(AppDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(AppDto), StatusCodes.Status200OK)]
@ -197,11 +185,9 @@ public sealed class AppsController : ApiController
/// </summary> /// </summary>
/// <param name="app">The name of the app to update.</param> /// <param name="app">The name of the app to update.</param>
/// <param name="file">The file to upload.</param> /// <param name="file">The file to upload.</param>
/// <returns> /// <response code="200">App image uploaded.</response>.
/// 200 => App image uploaded. /// <response code="400">App request not valid.</response>.
/// 400 => App request not valid. /// <response code="404">App not found.</response>.
/// 404 => App not found.
/// </returns>
[HttpPost] [HttpPost]
[Route("apps/{app}/image")] [Route("apps/{app}/image")]
[ProducesResponseType(typeof(AppDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(AppDto), StatusCodes.Status200OK)]
@ -218,10 +204,8 @@ public sealed class AppsController : ApiController
/// Remove the app image. /// Remove the app image.
/// </summary> /// </summary>
/// <param name="app">The name of the app to update.</param> /// <param name="app">The name of the app to update.</param>
/// <returns> /// <response code="200">App image removed.</response>.
/// 200 => App image removed. /// <response code="404">App not found.</response>.
/// 404 => App not found.
/// </returns>
[HttpDelete] [HttpDelete]
[Route("apps/{app}/image")] [Route("apps/{app}/image")]
[ProducesResponseType(typeof(AppDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(AppDto), StatusCodes.Status200OK)]
@ -238,12 +222,11 @@ public sealed class AppsController : ApiController
/// Delete the app. /// Delete the app.
/// </summary> /// </summary>
/// <param name="app">The name of the app to delete.</param> /// <param name="app">The name of the app to delete.</param>
/// <returns> /// <response code="204">App deleted.</response>.
/// 204 => App deleted. /// <response code="404">App not found.</response>.
/// 404 => App not found.
/// </returns>
[HttpDelete] [HttpDelete]
[Route("apps/{app}/")] [Route("apps/{app}/")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ApiPermission(PermissionIds.AppDelete)] [ApiPermission(PermissionIds.AppDelete)]
[ApiCosts(0)] [ApiCosts(0)]
public async Task<IActionResult> DeleteApp(string app) public async Task<IActionResult> DeleteApp(string app)

12
backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs

@ -52,10 +52,8 @@ public sealed class AssetContentController : ApiController
/// <param name="idOrSlug">The id or slug of the asset.</param> /// <param name="idOrSlug">The id or slug of the asset.</param>
/// <param name="request">The request parameters.</param> /// <param name="request">The request parameters.</param>
/// <param name="more">Optional suffix that can be used to seo-optimize the link to the image Has not effect.</param> /// <param name="more">Optional suffix that can be used to seo-optimize the link to the image Has not effect.</param>
/// <returns> /// <response code="200">Asset found and content or (resized) image returned.</response>.
/// 200 => Asset found and content or (resized) image returned. /// <response code="404">Asset or app not found.</response>.
/// 404 => Asset or app not found.
/// </returns>
[HttpGet] [HttpGet]
[Route("assets/{app}/{idOrSlug}/{*more}")] [Route("assets/{app}/{idOrSlug}/{*more}")]
[ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)] [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)]
@ -81,10 +79,8 @@ public sealed class AssetContentController : ApiController
/// </summary> /// </summary>
/// <param name="id">The ID of the asset.</param> /// <param name="id">The ID of the asset.</param>
/// <param name="request">The request parameters.</param> /// <param name="request">The request parameters.</param>
/// <returns> /// <response code="200">Asset found and content or (resized) image returned.</response>.
/// 200 => Asset found and content or (resized) image returned. /// <response code="404">Asset or app not found.</response>.
/// 404 => Asset or app not found.
/// </returns>
[HttpGet] [HttpGet]
[Route("assets/{id}/")] [Route("assets/{id}/")]
[ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)] [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)]

37
backend/src/Squidex/Areas/Api/Controllers/Assets/AssetFoldersController.cs

@ -39,10 +39,8 @@ public sealed class AssetFoldersController : ApiController
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="parentId">The optional parent folder id.</param> /// <param name="parentId">The optional parent folder id.</param>
/// <param name="scope">The scope of the query.</param> /// <param name="scope">The scope of the query.</param>
/// <returns> /// <response code="200">Asset folders returned.</response>.
/// 200 => Asset folders returned. /// <response code="404">App not found.</response>.
/// 404 => App not found.
/// </returns>
/// <remarks> /// <remarks>
/// Get all asset folders for the app. /// Get all asset folders for the app.
/// </remarks> /// </remarks>
@ -72,11 +70,9 @@ public sealed class AssetFoldersController : ApiController
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="request">The asset folder object that needs to be added to the App.</param> /// <param name="request">The asset folder object that needs to be added to the App.</param>
/// <returns> /// <response code="201">Asset folder created.</response>.
/// 201 => Asset folder created. /// <response code="400">Asset folder request not valid.</response>.
/// 400 => Asset folder request not valid. /// <response code="404">App not found.</response>.
/// 404 => App not found.
/// </returns>
[HttpPost] [HttpPost]
[Route("apps/{app}/assets/folders", Order = -1)] [Route("apps/{app}/assets/folders", Order = -1)]
[ProducesResponseType(typeof(AssetFolderDto), StatusCodes.Status201Created)] [ProducesResponseType(typeof(AssetFolderDto), StatusCodes.Status201Created)]
@ -98,11 +94,9 @@ public sealed class AssetFoldersController : ApiController
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="id">The ID of the asset folder.</param> /// <param name="id">The ID of the asset folder.</param>
/// <param name="request">The asset folder object that needs to updated.</param> /// <param name="request">The asset folder object that needs to updated.</param>
/// <returns> /// <response code="200">Asset folder updated.</response>.
/// 200 => Asset folder updated. /// <response code="400">Asset folder request not valid.</response>.
/// 400 => Asset folder request not valid. /// <response code="404">Asset folder or app not found.</response>.
/// 404 => Asset folder or app not found.
/// </returns>
[HttpPut] [HttpPut]
[Route("apps/{app}/assets/folders/{id}/", Order = -1)] [Route("apps/{app}/assets/folders/{id}/", Order = -1)]
[ProducesResponseType(typeof(AssetFolderDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(AssetFolderDto), StatusCodes.Status200OK)]
@ -124,11 +118,9 @@ public sealed class AssetFoldersController : ApiController
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="id">The ID of the asset folder.</param> /// <param name="id">The ID of the asset folder.</param>
/// <param name="request">The asset folder object that needs to updated.</param> /// <param name="request">The asset folder object that needs to updated.</param>
/// <returns> /// <response code="200">Asset folder moved.</response>.
/// 200 => Asset folder moved. /// <response code="400">Asset folder request not valid.</response>.
/// 400 => Asset folder request not valid. /// <response code="404">Asset folder or app not found.</response>.
/// 404 => Asset folder or app not found.
/// </returns>
[HttpPut] [HttpPut]
[Route("apps/{app}/assets/folders/{id}/parent", Order = -1)] [Route("apps/{app}/assets/folders/{id}/parent", Order = -1)]
[ProducesResponseType(typeof(AssetFolderDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(AssetFolderDto), StatusCodes.Status200OK)]
@ -149,12 +141,11 @@ public sealed class AssetFoldersController : ApiController
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="id">The ID of the asset folder to delete.</param> /// <param name="id">The ID of the asset folder to delete.</param>
/// <returns> /// <response code="204">Asset folder deleted.</response>.
/// 204 => Asset folder deleted. /// <response code="404">Asset folder or app not found.</response>.
/// 404 => Asset folder or app not found.
/// </returns>
[HttpDelete] [HttpDelete]
[Route("apps/{app}/assets/folders/{id}/", Order = -1)] [Route("apps/{app}/assets/folders/{id}/", Order = -1)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ApiPermissionOrAnonymous(PermissionIds.AppAssetsUpdate)] [ApiPermissionOrAnonymous(PermissionIds.AppAssetsUpdate)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> DeleteAssetFolder(string app, DomainId id) public async Task<IActionResult> DeleteAssetFolder(string app, DomainId id)

108
backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs

@ -8,7 +8,6 @@
using System.Globalization; using System.Globalization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
using NSwag.Annotations;
using Squidex.Areas.Api.Controllers.Assets.Models; using Squidex.Areas.Api.Controllers.Assets.Models;
using Squidex.Assets; using Squidex.Assets;
using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Core.Scripting;
@ -58,10 +57,8 @@ public sealed class AssetsController : ApiController
/// Get assets tags. /// Get assets tags.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <returns> /// <response code="200">Assets tags returned.</response>.
/// 200 => Assets tags returned. /// <response code="404">App not found.</response>.
/// 404 => App not found.
/// </returns>
/// <remarks> /// <remarks>
/// Get all tags for assets. /// Get all tags for assets.
/// </remarks> /// </remarks>
@ -85,10 +82,8 @@ public sealed class AssetsController : ApiController
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="name">The tag to return.</param> /// <param name="name">The tag to return.</param>
/// <param name="request">The required request object.</param> /// <param name="request">The required request object.</param>
/// <returns> /// <response code="200">Asset tag renamed and new tags returned.</response>.
/// 200 => Asset tag renamed and new tags returned. /// <response code="404">App not found.</response>.
/// 404 => App not found.
/// </returns>
[HttpPut] [HttpPut]
[Route("apps/{app}/assets/tags/{name}")] [Route("apps/{app}/assets/tags/{name}")]
[ProducesResponseType(typeof(Dictionary<string, int>), StatusCodes.Status200OK)] [ProducesResponseType(typeof(Dictionary<string, int>), StatusCodes.Status200OK)]
@ -108,10 +103,8 @@ public sealed class AssetsController : ApiController
/// <param name="parentId">The optional parent folder id.</param> /// <param name="parentId">The optional parent folder id.</param>
/// <param name="ids">The optional asset ids.</param> /// <param name="ids">The optional asset ids.</param>
/// <param name="q">The optional json query.</param> /// <param name="q">The optional json query.</param>
/// <returns> /// <response code="200">Assets returned.</response>.
/// 200 => Assets returned. /// <response code="404">App not found.</response>.
/// 404 => App not found.
/// </returns>
/// <remarks> /// <remarks>
/// Get all assets for the app. /// Get all assets for the app.
/// </remarks> /// </remarks>
@ -137,10 +130,8 @@ public sealed class AssetsController : ApiController
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="query">The required query object.</param> /// <param name="query">The required query object.</param>
/// <returns> /// <response code="200">Assets returned.</response>.
/// 200 => Assets returned. /// <response code="404">App not found.</response>.
/// 404 => App not found.
/// </returns>
/// <remarks> /// <remarks>
/// Get all assets for the app. /// Get all assets for the app.
/// </remarks> /// </remarks>
@ -166,10 +157,8 @@ public sealed class AssetsController : ApiController
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="id">The ID of the asset to retrieve.</param> /// <param name="id">The ID of the asset to retrieve.</param>
/// <returns> /// <response code="200">Asset found.</response>.
/// 200 => Asset found. /// <response code="404">Asset or app not found.</response>.
/// 404 => Asset or app not found.
/// </returns>
[HttpGet] [HttpGet]
[Route("apps/{app}/assets/{id}/")] [Route("apps/{app}/assets/{id}/")]
[ProducesResponseType(typeof(AssetDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(AssetDto), StatusCodes.Status200OK)]
@ -197,12 +186,10 @@ public sealed class AssetsController : ApiController
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="request">The request parameters.</param> /// <param name="request">The request parameters.</param>
/// <returns> /// <response code="201">Asset created.</response>.
/// 201 => Asset created. /// <response code="400">Asset request not valid.</response>.
/// 400 => Asset request not valid. /// <response code="413">Asset exceeds the maximum upload size.</response>.
/// 413 => Asset exceeds the maximum upload size. /// <response code="404">App not found.</response>.
/// 404 => App not found.
/// </returns>
/// <remarks> /// <remarks>
/// You can only upload one file at a time. The mime type of the file is not calculated by Squidex and is required correctly. /// You can only upload one file at a time. The mime type of the file is not calculated by Squidex and is required correctly.
/// </remarks> /// </remarks>
@ -225,16 +212,14 @@ public sealed class AssetsController : ApiController
/// Upload a new asset using tus.io. /// Upload a new asset using tus.io.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <returns> /// <response code="201">Asset created.</response>.
/// 201 => Asset created. /// <response code="400">Asset request not valid.</response>.
/// 400 => Asset request not valid. /// <response code="413">Asset exceeds the maximum upload size.</response>.
/// 413 => Asset exceeds the maximum upload size. /// <response code="404">App not found.</response>.
/// 404 => App not found.
/// </returns>
/// <remarks> /// <remarks>
/// Use the tus protocol to upload an asset. /// Use the tus protocol to upload an asset.
/// </remarks> /// </remarks>
[OpenApiIgnore] [ApiExplorerSettings(IgnoreApi = true)]
[Route("apps/{app}/assets/tus/{**fileId}")] [Route("apps/{app}/assets/tus/{**fileId}")]
[ProducesResponseType(typeof(AssetDto), StatusCodes.Status201Created)] [ProducesResponseType(typeof(AssetDto), StatusCodes.Status201Created)]
[AssetRequestSizeLimit] [AssetRequestSizeLimit]
@ -263,11 +248,9 @@ public sealed class AssetsController : ApiController
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="request">The bulk update request.</param> /// <param name="request">The bulk update request.</param>
/// <returns> /// <response code="200">Assets created, update or delete.</response>.
/// 200 => Assets created, update or delete. /// <response code="400">Assets request not valid.</response>.
/// 400 => Assets request not valid. /// <response code="404">App not found.</response>.
/// 404 => App not found.
/// </returns>
[HttpPost] [HttpPost]
[Route("apps/{app}/assets/bulk")] [Route("apps/{app}/assets/bulk")]
[ProducesResponseType(typeof(BulkResultDto[]), StatusCodes.Status200OK)] [ProducesResponseType(typeof(BulkResultDto[]), StatusCodes.Status200OK)]
@ -291,12 +274,10 @@ public sealed class AssetsController : ApiController
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="id">The optional custom asset id.</param> /// <param name="id">The optional custom asset id.</param>
/// <param name="request">The request parameters.</param> /// <param name="request">The request parameters.</param>
/// <returns> /// <response code="200">Asset created or updated.</response>.
/// 200 => Asset created or updated. /// <response code="400">Asset request not valid.</response>.
/// 400 => Asset request not valid. /// <response code="413">Asset exceeds the maximum upload size.</response>.
/// 413 => Asset exceeds the maximum upload size. /// <response code="404">App not found.</response>.
/// 404 => App not found.
/// </returns>
/// <remarks> /// <remarks>
/// You can only upload one file at a time. The mime type of the file is not calculated by Squidex and is required correctly. /// You can only upload one file at a time. The mime type of the file is not calculated by Squidex and is required correctly.
/// </remarks> /// </remarks>
@ -321,12 +302,10 @@ public sealed class AssetsController : ApiController
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="id">The ID of the asset.</param> /// <param name="id">The ID of the asset.</param>
/// <param name="file">The file to upload.</param> /// <param name="file">The file to upload.</param>
/// <returns> /// <response code="200">Asset updated.</response>.
/// 200 => Asset updated. /// <response code="400">Asset request not valid.</response>.
/// 400 => Asset request not valid. /// <response code="413">Asset exceeds the maximum upload size.</response>.
/// 413 => Asset exceeds the maximum upload size. /// <response code="404">Asset or app not found.</response>.
/// 404 => Asset or app not found.
/// </returns>
/// <remarks> /// <remarks>
/// Use multipart request to upload an asset. /// Use multipart request to upload an asset.
/// </remarks> /// </remarks>
@ -351,11 +330,9 @@ public sealed class AssetsController : ApiController
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="id">The ID of the asset.</param> /// <param name="id">The ID of the asset.</param>
/// <param name="request">The asset object that needs to updated.</param> /// <param name="request">The asset object that needs to updated.</param>
/// <returns> /// <response code="200">Asset updated.</response>.
/// 200 => Asset updated. /// <response code="400">Asset request not valid.</response>.
/// 400 => Asset request not valid. /// <response code="404">Asset or app not found.</response>.
/// 404 => Asset or app not found.
/// </returns>
[HttpPut] [HttpPut]
[Route("apps/{app}/assets/{id}/")] [Route("apps/{app}/assets/{id}/")]
[ProducesResponseType(typeof(AssetDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(AssetDto), StatusCodes.Status200OK)]
@ -377,11 +354,9 @@ public sealed class AssetsController : ApiController
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="id">The ID of the asset.</param> /// <param name="id">The ID of the asset.</param>
/// <param name="request">The asset object that needs to updated.</param> /// <param name="request">The asset object that needs to updated.</param>
/// <returns> /// <response code="200">Asset moved.</response>.
/// 200 => Asset moved. /// <response code="400">Asset request not valid.</response>.
/// 400 => Asset request not valid. /// <response code="404">Asset or app not found.</response>.
/// 404 => Asset or app not found.
/// </returns>
[HttpPut] [HttpPut]
[Route("apps/{app}/assets/{id}/parent")] [Route("apps/{app}/assets/{id}/parent")]
[ProducesResponseType(typeof(AssetDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(AssetDto), StatusCodes.Status200OK)]
@ -403,12 +378,11 @@ public sealed class AssetsController : ApiController
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="id">The ID of the asset to delete.</param> /// <param name="id">The ID of the asset to delete.</param>
/// <param name="request">The request parameters.</param> /// <param name="request">The request parameters.</param>
/// <returns> /// <response code="204">Asset deleted.</response>.
/// 204 => Asset deleted. /// <response code="404">Asset or app not found.</response>.
/// 404 => Asset or app not found.
/// </returns>
[HttpDelete] [HttpDelete]
[Route("apps/{app}/assets/{id}/")] [Route("apps/{app}/assets/{id}/")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ApiPermissionOrAnonymous(PermissionIds.AppAssetsDelete)] [ApiPermissionOrAnonymous(PermissionIds.AppAssetsDelete)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> DeleteAsset(string app, DomainId id, DeleteAssetDto request) public async Task<IActionResult> DeleteAsset(string app, DomainId id, DeleteAssetDto request)
@ -424,7 +398,7 @@ public sealed class AssetsController : ApiController
[Route("apps/{app}/assets/completion")] [Route("apps/{app}/assets/completion")]
[ApiPermissionOrAnonymous] [ApiPermissionOrAnonymous]
[ApiCosts(1)] [ApiCosts(1)]
[OpenApiIgnore] [ApiExplorerSettings(IgnoreApi = true)]
public IActionResult GetScriptCompletion(string app, string schema, public IActionResult GetScriptCompletion(string app, string schema,
[FromServices] ScriptingCompleter completer) [FromServices] ScriptingCompleter completer)
{ {
@ -437,7 +411,7 @@ public sealed class AssetsController : ApiController
[Route("apps/{app}/assets/completion/trigger")] [Route("apps/{app}/assets/completion/trigger")]
[ApiPermissionOrAnonymous] [ApiPermissionOrAnonymous]
[ApiCosts(1)] [ApiCosts(1)]
[OpenApiIgnore] [ApiExplorerSettings(IgnoreApi = true)]
public IActionResult GetScriptTriggerCompletion(string app, string schema, public IActionResult GetScriptTriggerCompletion(string app, string schema,
[FromServices] ScriptingCompleter completer) [FromServices] ScriptingCompleter completer)
{ {

2
backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetContentQueryDto.cs

@ -132,7 +132,7 @@ public sealed class AssetContentQueryDto
request.Headers.TryGetValue("Accept", out var accept); request.Headers.TryGetValue("Accept", out var accept);
return accept.Any(x => x.Contains(mimeType, StringComparison.OrdinalIgnoreCase)) && assetThumbnailGenerator.CanReadAndWrite(mimeType); return accept.Any(x => x?.Contains(mimeType, StringComparison.OrdinalIgnoreCase) == true) && assetThumbnailGenerator.CanReadAndWrite(mimeType);
} }
#if ENABLE_AVIF #if ENABLE_AVIF
if (Accepts("image/avif")) if (Accepts("image/avif"))

12
backend/src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs

@ -37,10 +37,8 @@ public class BackupContentController : ApiController
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="id">The ID of the backup.</param> /// <param name="id">The ID of the backup.</param>
/// <returns> /// <response code="200">Backup found and content returned.</response>.
/// 200 => Backup found and content returned. /// <response code="404">Backup or app not found.</response>.
/// 404 => Backup or app not found.
/// </returns>
[HttpGet] [HttpGet]
[Route("apps/{app}/backups/{id}")] [Route("apps/{app}/backups/{id}")]
[ResponseCache(Duration = 3600 * 24 * 30)] [ResponseCache(Duration = 3600 * 24 * 30)]
@ -58,10 +56,8 @@ public class BackupContentController : ApiController
/// <param name="id">The ID of the backup.</param> /// <param name="id">The ID of the backup.</param>
/// <param name="appId">The ID of the app.</param> /// <param name="appId">The ID of the app.</param>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <returns> /// <response code="200">Backup found and content returned.</response>.
/// 200 => Backup found and content returned. /// <response code="404">Backup or app not found.</response>.
/// 404 => Backup or app not found.
/// </returns>
[HttpGet] [HttpGet]
[Route("apps/backups/{id}")] [Route("apps/backups/{id}")]
[ResponseCache(Duration = 3600 * 24 * 30)] [ResponseCache(Duration = 3600 * 24 * 30)]

24
backend/src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs

@ -34,10 +34,8 @@ public class BackupsController : ApiController
/// Get all backup jobs. /// Get all backup jobs.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <returns> /// <response code="200">Backups returned.</response>.
/// 200 => Backups returned. /// <response code="404">App not found.</response>.
/// 404 => App not found.
/// </returns>
[HttpGet] [HttpGet]
[Route("apps/{app}/backups/")] [Route("apps/{app}/backups/")]
[ProducesResponseType(typeof(BackupJobsDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(BackupJobsDto), StatusCodes.Status200OK)]
@ -56,14 +54,12 @@ public class BackupsController : ApiController
/// Start a new backup. /// Start a new backup.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <returns> /// <response code="204">Backup started.</response>.
/// 204 => Backup started. /// <response code="400">Backup contingent reached.</response>.
/// 400 => Backup contingent reached. /// <response code="404">App not found.</response>.
/// 404 => App not found.
/// </returns>
[HttpPost] [HttpPost]
[Route("apps/{app}/backups/")] [Route("apps/{app}/backups/")]
[ProducesResponseType(typeof(BackupJobDto[]), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ApiPermissionOrAnonymous(PermissionIds.AppBackupsCreate)] [ApiPermissionOrAnonymous(PermissionIds.AppBackupsCreate)]
[ApiCosts(0)] [ApiCosts(0)]
public async Task<IActionResult> PostBackup(string app) public async Task<IActionResult> PostBackup(string app)
@ -78,13 +74,11 @@ public class BackupsController : ApiController
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="id">The ID of the backup to delete.</param> /// <param name="id">The ID of the backup to delete.</param>
/// <returns> /// <response code="204">Backup deleted.</response>.
/// 204 => Backup deleted. /// <response code="404">Backup or app not found.</response>.
/// 404 => Backup or app not found.
/// </returns>
[HttpDelete] [HttpDelete]
[Route("apps/{app}/backups/{id}")] [Route("apps/{app}/backups/{id}")]
[ProducesResponseType(typeof(BackupJobDto[]), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ApiPermissionOrAnonymous(PermissionIds.AppBackupsDelete)] [ApiPermissionOrAnonymous(PermissionIds.AppBackupsDelete)]
[ApiCosts(0)] [ApiCosts(0)]
public async Task<IActionResult> DeleteBackup(string app, DomainId id) public async Task<IActionResult> DeleteBackup(string app, DomainId id)

9
backend/src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs

@ -33,9 +33,7 @@ public class RestoreController : ApiController
/// <summary> /// <summary>
/// Get current restore status. /// Get current restore status.
/// </summary> /// </summary>
/// <returns> /// <response code="200">Status returned.</response>.
/// 200 => Status returned.
/// </returns>
[HttpGet] [HttpGet]
[Route("apps/restore/")] [Route("apps/restore/")]
[ProducesResponseType(typeof(RestoreJobDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(RestoreJobDto), StatusCodes.Status200OK)]
@ -58,11 +56,10 @@ public class RestoreController : ApiController
/// Restore a backup. /// Restore a backup.
/// </summary> /// </summary>
/// <param name="request">The backup to restore.</param> /// <param name="request">The backup to restore.</param>
/// <returns> /// <response code="204">Restore operation started.</response>.
/// 204 => Restore operation started.
/// </returns>
[HttpPost] [HttpPost]
[Route("apps/restore/")] [Route("apps/restore/")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ApiPermission(PermissionIds.AdminRestore)] [ApiPermission(PermissionIds.AdminRestore)]
public async Task<IActionResult> PostRestoreJob([FromBody] RestoreRequestDto request) public async Task<IActionResult> PostRestoreJob([FromBody] RestoreRequestDto request)
{ {

36
backend/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs

@ -41,10 +41,8 @@ public sealed class CommentsController : ApiController
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="resource">The path to the resource.</param> /// <param name="resource">The path to the resource.</param>
/// <returns> /// <response code="200">Watching users returned.</response>.
/// 200 => Watching users returned. /// <response code="404">App not found.</response>.
/// 404 => App not found.
/// </returns>
[HttpGet] [HttpGet]
[Route("apps/{app}/watching/{*resource}")] [Route("apps/{app}/watching/{*resource}")]
[ProducesResponseType(typeof(string[]), StatusCodes.Status200OK)] [ProducesResponseType(typeof(string[]), StatusCodes.Status200OK)]
@ -66,10 +64,8 @@ public sealed class CommentsController : ApiController
/// <remarks> /// <remarks>
/// When passing in a version you can retrieve all updates since then. /// When passing in a version you can retrieve all updates since then.
/// </remarks> /// </remarks>
/// <returns> /// <response code="200">Comments returned.</response>.
/// 200 => Comments returned. /// <response code="404">App not found.</response>.
/// 404 => App not found.
/// </returns>
[HttpGet] [HttpGet]
[Route("apps/{app}/comments/{commentsId}")] [Route("apps/{app}/comments/{commentsId}")]
[ProducesResponseType(typeof(CommentsDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(CommentsDto), StatusCodes.Status200OK)]
@ -95,11 +91,9 @@ public sealed class CommentsController : ApiController
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="commentsId">The ID of the comments.</param> /// <param name="commentsId">The ID of the comments.</param>
/// <param name="request">The comment object that needs to created.</param> /// <param name="request">The comment object that needs to created.</param>
/// <returns> /// <response code="201">Comment created.</response>.
/// 201 => Comment created. /// <response code="400">Comment request not valid.</response>.
/// 400 => Comment request not valid. /// <response code="404">App not found.</response>.
/// 404 => App not found.
/// </returns>
[HttpPost] [HttpPost]
[Route("apps/{app}/comments/{commentsId}")] [Route("apps/{app}/comments/{commentsId}")]
[ProducesResponseType(typeof(CommentDto), 201)] [ProducesResponseType(typeof(CommentDto), 201)]
@ -123,13 +117,12 @@ public sealed class CommentsController : ApiController
/// <param name="commentsId">The ID of the comments.</param> /// <param name="commentsId">The ID of the comments.</param>
/// <param name="commentId">The ID of the comment.</param> /// <param name="commentId">The ID of the comment.</param>
/// <param name="request">The comment object that needs to updated.</param> /// <param name="request">The comment object that needs to updated.</param>
/// <returns> /// <response code="204">Comment updated.</response>.
/// 204 => Comment updated. /// <response code="400">Comment request not valid.</response>.
/// 400 => Comment request not valid. /// <response code="404">Comment or app not found.</response>.
/// 404 => Comment or app not found.
/// </returns>
[HttpPut] [HttpPut]
[Route("apps/{app}/comments/{commentsId}/{commentId}")] [Route("apps/{app}/comments/{commentsId}/{commentId}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ApiPermissionOrAnonymous(PermissionIds.AppCommentsUpdate)] [ApiPermissionOrAnonymous(PermissionIds.AppCommentsUpdate)]
[ApiCosts(0)] [ApiCosts(0)]
public async Task<IActionResult> PutComment(string app, DomainId commentsId, DomainId commentId, [FromBody] UpsertCommentDto request) public async Task<IActionResult> PutComment(string app, DomainId commentsId, DomainId commentId, [FromBody] UpsertCommentDto request)
@ -147,12 +140,11 @@ public sealed class CommentsController : ApiController
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="commentsId">The ID of the comments.</param> /// <param name="commentsId">The ID of the comments.</param>
/// <param name="commentId">The ID of the comment.</param> /// <param name="commentId">The ID of the comment.</param>
/// <returns> /// <response code="204">Comment deleted.</response>.
/// 204 => Comment deleted. /// <response code="404">Comment or app not found.</response>.
/// 404 => Comment or app not found.
/// </returns>
[HttpDelete] [HttpDelete]
[Route("apps/{app}/comments/{commentsId}/{commentId}")] [Route("apps/{app}/comments/{commentsId}/{commentId}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ApiPermissionOrAnonymous(PermissionIds.AppCommentsDelete)] [ApiPermissionOrAnonymous(PermissionIds.AppCommentsDelete)]
[ApiCosts(0)] [ApiCosts(0)]
public async Task<IActionResult> DeleteComment(string app, DomainId commentsId, DomainId commentId) public async Task<IActionResult> DeleteComment(string app, DomainId commentsId, DomainId commentId)

11
backend/src/Squidex/Areas/Api/Controllers/Comments/Notifications/UserNotificationsController.cs

@ -41,9 +41,7 @@ public sealed class UserNotificationsController : ApiController
/// <remarks> /// <remarks>
/// When passing in a version you can retrieve all updates since then. /// When passing in a version you can retrieve all updates since then.
/// </remarks> /// </remarks>
/// <returns> /// <response code="200">All comments returned.</response>.
/// 200 => All comments returned.
/// </returns>
[HttpGet] [HttpGet]
[Route("users/{userId}/notifications")] [Route("users/{userId}/notifications")]
[ProducesResponseType(typeof(CommentsDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(CommentsDto), StatusCodes.Status200OK)]
@ -69,12 +67,11 @@ public sealed class UserNotificationsController : ApiController
/// </summary> /// </summary>
/// <param name="userId">The user id.</param> /// <param name="userId">The user id.</param>
/// <param name="commentId">The ID of the comment.</param> /// <param name="commentId">The ID of the comment.</param>
/// <returns> /// <response code="204">Comment deleted.</response>.
/// 204 => Comment deleted. /// <response code="404">Comment not found.</response>.
/// 404 => Comment not found.
/// </returns>
[HttpDelete] [HttpDelete]
[Route("users/{userId}/notifications/{commentId}")] [Route("users/{userId}/notifications/{commentId}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ApiPermission] [ApiPermission]
public async Task<IActionResult> DeleteComment(DomainId userId, DomainId commentId) public async Task<IActionResult> DeleteComment(DomainId userId, DomainId commentId)
{ {

130
backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs

@ -40,10 +40,8 @@ public sealed class ContentsController : ApiController
/// <param name="schema">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="ids">The optional ids of the content to fetch.</param> /// <param name="ids">The optional ids of the content to fetch.</param>
/// <param name="q">The optional json query.</param> /// <param name="q">The optional json query.</param>
/// <returns> /// <response code="200">Contents retunred.</response>.
/// 200 => Contents retunred. /// <response code="404">Schema or app not found.</response>.
/// 404 => Schema or app not found.
/// </returns>
/// <remarks> /// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs. /// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks> /// </remarks>
@ -70,10 +68,8 @@ public sealed class ContentsController : ApiController
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="schema">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="query">The required query object.</param> /// <param name="query">The required query object.</param>
/// <returns> /// <response code="200">Contents returned.</response>.
/// 200 => Contents returned. /// <response code="404">Schema or app not found.</response>.
/// 404 => Schema or app not found.
/// </returns>
/// <remarks> /// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs. /// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks> /// </remarks>
@ -101,10 +97,8 @@ public sealed class ContentsController : ApiController
/// <param name="schema">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="id">The ID of the content to fetch.</param> /// <param name="id">The ID of the content to fetch.</param>
/// <param name="version">The optional version.</param> /// <param name="version">The optional version.</param>
/// <returns> /// <response code="200">Content returned.</response>.
/// 200 => Content returned. /// <response code="404">Content, schema or app not found.</response>.
/// 404 => Content, schema or app not found.
/// </returns>
/// <remarks> /// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs. /// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks> /// </remarks>
@ -136,16 +130,15 @@ public sealed class ContentsController : ApiController
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="schema">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="id">The ID of the content to fetch.</param> /// <param name="id">The ID of the content to fetch.</param>
/// <returns> /// <response code="204">Content is valid.</response>.
/// 204 => Content is valid. /// <response code="400">Content not valid.</response>.
/// 400 => Content not valid. /// <response code="404">Content, schema or app not found.</response>.
/// 404 => Content, schema or app not found.
/// </returns>
/// <remarks> /// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs. /// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks> /// </remarks>
[HttpGet] [HttpGet]
[Route("content/{app}/{schema}/{id}/validity")] [Route("content/{app}/{schema}/{id}/validity")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ApiPermissionOrAnonymous] [ApiPermissionOrAnonymous]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> GetContentValidity(string app, string schema, DomainId id) public async Task<IActionResult> GetContentValidity(string app, string schema, DomainId id)
@ -164,10 +157,8 @@ public sealed class ContentsController : ApiController
/// <param name="schema">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="id">The ID of the content to fetch.</param> /// <param name="id">The ID of the content to fetch.</param>
/// <param name="q">The optional json query.</param> /// <param name="q">The optional json query.</param>
/// <returns> /// <response code="200">Contents returned.</response>.
/// 200 => Contents returned. /// <response code="404">Content, schema or app not found.</response>.
/// 404 => Content, schema or app not found.
/// </returns>
/// <remarks> /// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs. /// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks> /// </remarks>
@ -195,10 +186,8 @@ public sealed class ContentsController : ApiController
/// <param name="schema">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="id">The ID of the content to fetch.</param> /// <param name="id">The ID of the content to fetch.</param>
/// <param name="q">The optional json query.</param> /// <param name="q">The optional json query.</param>
/// <returns> /// <response code="200">Content returned.</response>.
/// 200 => Content returned. /// <response code="404">Content, schema or app not found.</response>.
/// 404 => Content, schema or app not found.
/// </returns>
/// <remarks> /// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs. /// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks> /// </remarks>
@ -226,10 +215,8 @@ public sealed class ContentsController : ApiController
/// <param name="schema">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="id">The ID of the content to fetch.</param> /// <param name="id">The ID of the content to fetch.</param>
/// <param name="version">The version fo the content to fetch.</param> /// <param name="version">The version fo the content to fetch.</param>
/// <returns> /// <response code="200">Content version returned.</response>.
/// 200 => Content version returned. /// <response code="404">Content, schema or app not found.</response>.
/// 404 => Content, schema or app not found.
/// </returns>
/// <remarks> /// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs. /// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks> /// </remarks>
@ -261,11 +248,9 @@ public sealed class ContentsController : ApiController
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="schema">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="request">The request parameters.</param> /// <param name="request">The request parameters.</param>
/// <returns> /// <response code="201">Content created.</response>.
/// 201 => Content created. /// <response code="400">Content request not valid.</response>.
/// 400 => Content request not valid. /// <response code="404">Content, schema or app not found.</response>.
/// 404 => Content, schema or app not found.
/// </returns>
/// <remarks> /// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs. /// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks> /// </remarks>
@ -289,11 +274,9 @@ public sealed class ContentsController : ApiController
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="schema">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="request">The import request.</param> /// <param name="request">The import request.</param>
/// <returns> /// <response code="200">Contents created.</response>.
/// 200 => Contents created. /// <response code="400">Content request not valid.</response>.
/// 400 => Content request not valid. /// <response code="404">Content references, schema or app not found.</response>.
/// 404 => Content references, schema or app not found.
/// </returns>
/// <remarks> /// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs. /// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks> /// </remarks>
@ -321,11 +304,9 @@ public sealed class ContentsController : ApiController
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="schema">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="request">The bulk update request.</param> /// <param name="request">The bulk update request.</param>
/// <returns> /// <response code="201">Contents created, update or delete.</response>.
/// 201 => Contents created, update or delete. /// <response code="400">Contents request not valid.</response>.
/// 400 => Contents request not valid. /// <response code="404">Contents references, schema or app not found.</response>.
/// 404 => Contents references, schema or app not found.
/// </returns>
/// <remarks> /// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs. /// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks> /// </remarks>
@ -353,11 +334,9 @@ public sealed class ContentsController : ApiController
/// <param name="schema">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="id">The ID of the content item to update.</param> /// <param name="id">The ID of the content item to update.</param>
/// <param name="request">The request parameters.</param> /// <param name="request">The request parameters.</param>
/// <returns> /// <response code="200">Content created or updated.</response>.
/// 200 => Content created or updated. /// <response code="400">Content request not valid.</response>.
/// 400 => Content request not valid. /// <response code="404">Content references, schema or app not found.</response>.
/// 404 => Content references, schema or app not found.
/// </returns>
/// <remarks> /// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs. /// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks> /// </remarks>
@ -382,11 +361,9 @@ public sealed class ContentsController : ApiController
/// <param name="schema">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="id">The ID of the content item to update.</param> /// <param name="id">The ID of the content item to update.</param>
/// <param name="request">The full data for the content item.</param> /// <param name="request">The full data for the content item.</param>
/// <returns> /// <response code="200">Content updated.</response>.
/// 200 => Content updated. /// <response code="400">Content request not valid.</response>.
/// 400 => Content request not valid. /// <response code="404">Content references, schema or app not found.</response>.
/// 404 => Content references, schema or app not found.
/// </returns>
/// <remarks> /// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs. /// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks> /// </remarks>
@ -411,11 +388,9 @@ public sealed class ContentsController : ApiController
/// <param name="schema">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="id">The ID of the content item to patch.</param> /// <param name="id">The ID of the content item to patch.</param>
/// <param name="request">The patch for the content item.</param> /// <param name="request">The patch for the content item.</param>
/// <returns> /// <response code="200">Content patched.</response>.
/// 200 => Content patched. /// <response code="400">Content request not valid.</response>.
/// 400 => Content request not valid. /// <response code="404">Content, schema or app not found.</response>.
/// 404 => Content, schema or app not found.
/// </returns>
/// <remarks> /// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs. /// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks> /// </remarks>
@ -440,11 +415,9 @@ public sealed class ContentsController : ApiController
/// <param name="schema">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="id">The ID of the content item to change.</param> /// <param name="id">The ID of the content item to change.</param>
/// <param name="request">The status request.</param> /// <param name="request">The status request.</param>
/// <returns> /// <response code="200">Content status changed.</response>.
/// 200 => Content status changed. /// <response code="400">Content request not valid.</response>.
/// 400 => Content request not valid. /// <response code="404">Content, schema or app not found.</response>.
/// 404 => Content, schema or app not found.
/// </returns>
/// <remarks> /// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs. /// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks> /// </remarks>
@ -468,11 +441,9 @@ public sealed class ContentsController : ApiController
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="schema">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="id">The ID of the content item to cancel.</param> /// <param name="id">The ID of the content item to cancel.</param>
/// <returns> /// <response code="200">Content status change cancelled.</response>.
/// 200 => Content status change cancelled. /// <response code="400">Content request not valid.</response>.
/// 400 => Content request not valid. /// <response code="404">Content, schema or app not found.</response>.
/// 404 => Content, schema or app not found.
/// </returns>
/// <remarks> /// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs. /// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks> /// </remarks>
@ -496,10 +467,8 @@ public sealed class ContentsController : ApiController
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="schema">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="id">The ID of the content item to create the draft for.</param> /// <param name="id">The ID of the content item to create the draft for.</param>
/// <returns> /// <response code="200">Content draft created.</response>.
/// 200 => Content draft created. /// <response code="404">Content, schema or app not found.</response>.
/// 404 => Content, schema or app not found.
/// </returns>
/// <remarks> /// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs. /// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks> /// </remarks>
@ -523,10 +492,8 @@ public sealed class ContentsController : ApiController
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="schema">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="id">The ID of the content item to delete the draft from.</param> /// <param name="id">The ID of the content item to delete the draft from.</param>
/// <returns> /// <response code="200">Content draft deleted.</response>.
/// 200 => Content draft deleted. /// <response code="404">Content, schema or app not found.</response>.
/// 404 => Content, schema or app not found.
/// </returns>
/// <remarks> /// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs. /// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks> /// </remarks>
@ -551,16 +518,15 @@ public sealed class ContentsController : ApiController
/// <param name="schema">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="id">The ID of the content item to delete.</param> /// <param name="id">The ID of the content item to delete.</param>
/// <param name="request">The request parameters.</param> /// <param name="request">The request parameters.</param>
/// <returns> /// <response code="204">Content deleted.</response>.
/// 204 => Content deleted. /// <response code="400">Content cannot be deleted.</response>.
/// 400 => Content cannot be deleted. /// <response code="404">Content, schema or app not found.</response>.
/// 404 => Content, schema or app not found.
/// </returns>
/// <remarks> /// <remarks>
/// You can create an generated documentation for your app at /api/content/{appName}/docs. /// You can create an generated documentation for your app at /api/content/{appName}/docs.
/// </remarks> /// </remarks>
[HttpDelete] [HttpDelete]
[Route("content/{app}/{schema}/{id}/")] [Route("content/{app}/{schema}/{id}/")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ApiPermissionOrAnonymous(PermissionIds.AppContentsDeleteOwn)] [ApiPermissionOrAnonymous(PermissionIds.AppContentsDeleteOwn)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> DeleteContent(string app, string schema, DomainId id, DeleteContentDto request) public async Task<IActionResult> DeleteContent(string app, string schema, DomainId id, DeleteContentDto request)

26
backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsSharedController.cs

@ -38,10 +38,8 @@ public sealed class ContentsSharedController : ApiController
/// GraphQL endpoint. /// GraphQL endpoint.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <returns> /// <response code="200">Contents returned or mutated.</response>.
/// 200 => Contents returned or mutated. /// <response code="404">App not found.</response>.
/// 404 => App not found.
/// </returns>
/// <remarks> /// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs. /// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks> /// </remarks>
@ -61,10 +59,8 @@ public sealed class ContentsSharedController : ApiController
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="query">The required query object.</param> /// <param name="query">The required query object.</param>
/// <returns> /// <response code="200">Contents returned.</response>.
/// 200 => Contents returned. /// <response code="404">App not found.</response>.
/// 404 => App not found.
/// </returns>
/// <remarks> /// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs. /// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks> /// </remarks>
@ -90,10 +86,8 @@ public sealed class ContentsSharedController : ApiController
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="query">The required query object.</param> /// <param name="query">The required query object.</param>
/// <returns> /// <response code="200">Contents returned.</response>.
/// 200 => Contents returned. /// <response code="404">App not found.</response>.
/// 404 => App not found.
/// </returns>
/// <remarks> /// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs. /// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks> /// </remarks>
@ -120,11 +114,9 @@ public sealed class ContentsSharedController : ApiController
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="schema">The name of the schema.</param> /// <param name="schema">The name of the schema.</param>
/// <param name="request">The bulk update request.</param> /// <param name="request">The bulk update request.</param>
/// <returns> /// <response code="201">Contents created, update or delete.</response>.
/// 201 => Contents created, update or delete. /// <response code="400">Contents request not valid.</response>.
/// 400 => Contents request not valid. /// <response code="404">Contents references, schema or app not found.</response>.
/// 404 => Contents references, schema or app not found.
/// </returns>
/// <remarks> /// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs. /// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks> /// </remarks>

14
backend/src/Squidex/Areas/Api/Controllers/Diagnostics/DiagnosticsController.cs

@ -30,12 +30,11 @@ public sealed class DiagnosticsController : ApiController
/// <summary> /// <summary>
/// Creates a dump and writes it into storage.. /// Creates a dump and writes it into storage..
/// </summary> /// </summary>
/// <returns> /// <response code="204">Dump created successful.</response>.
/// 204 => Dump created successful. /// <response code="501">Not configured.</response>.
/// 501 => Not configured.
/// </returns>
[HttpGet] [HttpGet]
[Route("diagnostics/dump")] [Route("diagnostics/dump")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ApiPermissionOrAnonymous(PermissionIds.Admin)] [ApiPermissionOrAnonymous(PermissionIds.Admin)]
public async Task<IActionResult> GetDump() public async Task<IActionResult> GetDump()
{ {
@ -52,12 +51,11 @@ public sealed class DiagnosticsController : ApiController
/// <summary> /// <summary>
/// Creates a gc dump and writes it into storage. /// Creates a gc dump and writes it into storage.
/// </summary> /// </summary>
/// <returns> /// <response code="204">Dump created successful.</response>.
/// 204 => Dump created successful. /// <response code="501">Not configured.</response>.
/// 501 => Not configured.
/// </returns>
[HttpGet] [HttpGet]
[Route("diagnostics/gcdump")] [Route("diagnostics/gcdump")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ApiPermissionOrAnonymous(PermissionIds.Admin)] [ApiPermissionOrAnonymous(PermissionIds.Admin)]
public async Task<IActionResult> GetGCDump() public async Task<IActionResult> GetGCDump()
{ {

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save