Browse Source

Merge branch 'release/4.x'

# Conflicts:
#	backend/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs
#	backend/src/Squidex.Domain.Apps.Entities/Contents/BulkUpdateCommandMiddleware.cs
#	backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/BulkUpdateContents.cs
#	backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveAssets.cs
#	backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveReferences.cs
#	backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/IUpsertCommand.cs
#	backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasIndex.cs
#	backend/src/Squidex.Infrastructure.MongoDb/Assets/MongoGridFsAssetStore.cs
#	backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs
#	backend/src/Squidex.Infrastructure/Translations/ResourcesLocalizer.cs
#	backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs
#	backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateWorkflowDto.cs
#	backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs
#	backend/src/Squidex/Areas/Api/Controllers/QueryDto.cs
#	backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/ReferencesValidatorTests.cs
#	backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValidatorTests.cs
#	backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DefaultAppLogStoreTests.cs
#	backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs
#	backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryFixture.cs
#	backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ResolveReferencesTests.cs
#	backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferenceFluidExtensionTests.cs
#	backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/CachingTextIndexerStateTests.cs
#	backend/tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs
pull/590/head
Sebastian 5 years ago
parent
commit
745440a444
  1. 2
      backend/extensions/Squidex.Extensions/APM/Datadog/DatadogPlugin.cs
  2. 2
      backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaProducer.cs
  3. 10
      backend/i18n/frontend_en.json
  4. 10
      backend/i18n/frontend_it.json
  5. 10
      backend/i18n/frontend_nl.json
  6. 2
      backend/i18n/source/backend_en.json
  7. 2
      backend/i18n/source/backend_it.json
  8. 2
      backend/i18n/source/backend_nl.json
  9. 10
      backend/i18n/source/frontend_en.json
  10. 2
      backend/src/Migrations/OldEvents/SchemaCreated.cs
  11. 11
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs
  12. 13
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs
  13. 4
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs
  14. 21
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonRole.cs
  15. 72
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/RoleConverter.cs
  16. 22
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/RolesConverter.cs
  17. 65
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs
  18. 76
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs
  19. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs
  20. 10
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs
  21. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptOperations.cs
  22. 2
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/FullText/MongoTextIndex.cs
  23. 11
      backend/src/Squidex.Domain.Apps.Entities/Apps/AppSettingsSearchSource.cs
  24. 3
      backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateLanguage.cs
  25. 4
      backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateRole.cs
  26. 2
      backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppLanguages.cs
  27. 6
      backend/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs
  28. 12
      backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/SchemaBuilder.cs
  29. 6
      backend/src/Squidex.Domain.Apps.Entities/Contents/BulkUpdateCommandMiddleware.cs
  30. 3
      backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/BulkUpdateContents.cs
  31. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs
  32. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs
  33. 10
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentActions.cs
  34. 5
      backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureFieldRules.cs
  35. 7
      backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchema.cs
  36. 9
      backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/IUpsertCommand.cs
  37. 4
      backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ReorderFields.cs
  38. 7
      backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/SynchronizeSchema.cs
  39. 4
      backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaField.cs
  40. 12
      backend/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs
  41. 4
      backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasIndex.cs
  42. 11
      backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs
  43. 3
      backend/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs
  44. 3
      backend/src/Squidex.Domain.Apps.Events/Apps/AppLanguageUpdated.cs
  45. 6
      backend/src/Squidex.Domain.Apps.Events/Apps/AppPlanChanged.cs
  46. 9
      backend/src/Squidex.Domain.Apps.Events/Apps/AppRoleUpdated.cs
  47. 4
      backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedField.cs
  48. 3
      backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaFieldsReordered.cs
  49. 4
      backend/src/Squidex.Infrastructure.MongoDb/Assets/MongoGridFsAssetStore.cs
  50. 18
      backend/src/Squidex.Infrastructure/Assets/ImageFormat.cs
  51. 2
      backend/src/Squidex.Infrastructure/Assets/ImageInfo.cs
  52. 57
      backend/src/Squidex.Infrastructure/Assets/ImageSharp/ImageSharpAssetThumbnailGenerator.cs
  53. 10
      backend/src/Squidex.Infrastructure/Assets/ResizeOptions.cs
  54. 12
      backend/src/Squidex.Infrastructure/EventSourcing/Grains/BatchSubscriber.cs
  55. 2
      backend/src/Squidex.Infrastructure/Log/ISemanticLog.cs
  56. 6
      backend/src/Squidex.Infrastructure/Tasks/AsyncHelper.cs
  57. 22
      backend/src/Squidex.Shared/Permissions.cs
  58. 2
      backend/src/Squidex.Shared/Texts.it.resx
  59. 2
      backend/src/Squidex.Shared/Texts.nl.resx
  60. 2
      backend/src/Squidex.Shared/Texts.resx
  61. 22
      backend/src/Squidex.Web/Pipeline/AppResolver.cs
  62. 25
      backend/src/Squidex/Areas/Api/Config/OpenApi/OpenApiServices.cs
  63. 2
      backend/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs
  64. 2
      backend/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs
  65. 2
      backend/src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs
  66. 14
      backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs
  67. 37
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs
  68. 15
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/RoleDto.cs
  69. 3
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateLanguageDto.cs
  70. 8
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateRoleDto.cs
  71. 2
      backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs
  72. 6
      backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetContentQueryDto.cs
  73. 8
      backend/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs
  74. 5
      backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkUpdateDto.cs
  75. 1
      backend/src/Squidex/Areas/Api/Controllers/Contents/Models/GraphQLGetDto.cs
  76. 1
      backend/src/Squidex/Areas/Api/Controllers/Contents/Models/GraphQLPostDto.cs
  77. 2
      backend/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs
  78. 2
      backend/src/Squidex/Areas/Api/Controllers/Ping/PingController.cs
  79. 3
      backend/src/Squidex/Areas/Api/Controllers/QueryDto.cs
  80. 5
      backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/ConfigureFieldRulesDto.cs
  81. 3
      backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/ReorderFieldsDto.cs
  82. 22
      backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaDto.cs
  83. 3
      backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaFieldDto.cs
  84. 4
      backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs
  85. 4
      backend/src/Squidex/Areas/Api/Controllers/Search/SearchController.cs
  86. 8
      backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs
  87. 4
      backend/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs
  88. 2
      backend/src/Squidex/Areas/IdentityServer/Views/Account/Login.cshtml
  89. 2
      backend/src/Squidex/Areas/IdentityServer/Views/Error/Error.cshtml
  90. 2
      backend/src/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml
  91. 1
      backend/src/Squidex/Config/Domain/SerializationServices.cs
  92. 22
      backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RoleTests.cs
  93. 39
      backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesJsonTests.cs
  94. 51
      backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs
  95. 10
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/SchemaSynchronizerTests.cs
  96. 4
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/ExpressionsAttribute.cs
  97. 2
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterCompareTests.cs
  98. 2
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs
  99. 3
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineHelperTests.cs
  100. 3
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs

2
backend/extensions/Squidex.Extensions/APM/Datadog/DatadogPlugin.cs

@ -15,7 +15,7 @@ using Microsoft.Extensions.DependencyInjection;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Plugins;
namespace Squidex.Extensions.APM.Datadoq
namespace Squidex.Extensions.APM.Datadog
{
public sealed class DatadogPlugin : IPlugin, IStartupFilter
{

2
backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaProducer.cs

@ -161,7 +161,7 @@ namespace Squidex.Extensions.Actions.Kafka
{
try
{
var schema = (RecordSchema)Avro.Schema.Parse(avroSchema);
var schema = (RecordSchema)Schema.Parse(avroSchema);
var jsonObject = jsonSerializer.Deserialize<JsonObject>(json);

10
backend/i18n/frontend_en.json

@ -550,6 +550,16 @@
"roles.deleteConfirmTitle": "Do you really want to delete the role?",
"roles.loadFailed": "Failed to load roles. Please reload.",
"roles.loadPermissionsFailed": "Failed to load permissions. Please reload.",
"roles.permissions": "Permissions",
"roles.permissionsDescription": "Permissions restrict the allowed operations and queries at API level and are a security feature.",
"roles.permissionsPlaceholder": "Start typing to search for permissions",
"roles.properties": "Properties",
"roles.properties.hideAPI": "Hide API",
"roles.properties.hideAssets": "Hide Assets",
"roles.properties.hideContents": "Hide {schema} Contents",
"roles.properties.hideSchemas": "Hide Schemas",
"roles.properties.hideSettings": "Hide Settings",
"roles.propertiesDescription": "Properties describe the behavior of the Management UI, but do not provide security for the API.",
"roles.refreshTooltip": "Refresh roles (CTRL + SHIFT + R)",
"roles.reloaded": "Roles reloaded.",
"roles.revokeFailed": "Failed to revoke role. Please reload.",

10
backend/i18n/frontend_it.json

@ -550,6 +550,16 @@
"roles.deleteConfirmTitle": "Sei sicuro di voler eliminare il ruolo?",
"roles.loadFailed": "Non è stato possibile caricare i ruoli. Per favore ricarica.",
"roles.loadPermissionsFailed": "Non è stato possibile caricare i permessi. Per favore ricarica.",
"roles.permissions": "Permissions",
"roles.permissionsDescription": "Permissions restrict the allowed operations and queries at API level and are a security feature.",
"roles.permissionsPlaceholder": "Start typing to search for permissions",
"roles.properties": "Properties",
"roles.properties.hideAPI": "Hide API",
"roles.properties.hideAssets": "Hide Assets",
"roles.properties.hideContents": "Hide {schema} Contents",
"roles.properties.hideSchemas": "Hide Schemas",
"roles.properties.hideSettings": "Hide Settings",
"roles.propertiesDescription": "Properties describe the behavior of the Management UI, but do not provide security for the API.",
"roles.refreshTooltip": "Aggiorna i ruoli (CTRL + SHIFT + R)",
"roles.reloaded": "Ruoli ricaricati.",
"roles.revokeFailed": "Non è stato possibile rimuovere il ruolo. Per favore ricarica.",

10
backend/i18n/frontend_nl.json

@ -550,6 +550,16 @@
"roles.deleteConfirmTitle": "Wil je de rol echt verwijderen?",
"roles.loadFailed": "Laden van rollen is mislukt. Laad opnieuw.",
"roles.loadPermissionsFailed": "Kan machtigingen niet laden. Laad opnieuw.",
"roles.permissions": "Permissions",
"roles.permissionsDescription": "Permissions restrict the allowed operations and queries at API level and are a security feature.",
"roles.permissionsPlaceholder": "Start typing to search for permissions",
"roles.properties": "Properties",
"roles.properties.hideAPI": "Hide API",
"roles.properties.hideAssets": "Hide Assets",
"roles.properties.hideContents": "Hide {schema} Contents",
"roles.properties.hideSchemas": "Hide Schemas",
"roles.properties.hideSettings": "Hide Settings",
"roles.propertiesDescription": "Properties describe the behavior of the Management UI, but do not provide security for the API.",
"roles.refreshTooltip": "Ververs rollen (CTRL + SHIFT + R)",
"roles.reloaded": "Rollen opnieuw geladen.",
"roles.revokeFailed": "Kan rol niet intrekken. Laad opnieuw.",

2
backend/i18n/source/backend_en.json

@ -66,7 +66,7 @@
"common.httpValidationError": "Validation error",
"common.initialStep": "Initial step",
"common.jsError": "Failed to execute script with Javascript error: {message}",
"common.jsNotAlloweed": "Script has forbidden the operation.",
"common.jsNotAllowed": "Script has forbidden the operation.",
"common.jsParseError": "Failed to execute script with Javascript syntax error: {message}",
"common.jsRejected": "Script rejected the operation.",
"common.language": "Language code",

2
backend/i18n/source/backend_it.json

@ -66,7 +66,7 @@
"common.httpValidationError": "Errore di validazione",
"common.initialStep": "Step iniziale",
"common.jsError": "Esecuzione dello script fallita, Errore Javascript: {message}",
"common.jsNotAlloweed": "Uno script ha proibito l'operazione.",
"common.jsNotAllowed": "Uno script ha proibito l'operazione.",
"common.jsParseError": "Esecuzione dello script fallita, errore di sintassi nel Javascript: {message}",
"common.jsRejected": "Lo script ha rifiutato l'operazione.",
"common.language": "Codice della lingua",

2
backend/i18n/source/backend_nl.json

@ -61,7 +61,7 @@
"common.httpValidationError": "Validatiefout",
"common.initialStep": "Eerste stap",
"common.jsError": "Kan script niet uitvoeren met Javascript-fout: {message}",
"common.jsNotAlloweed": "Script heeft de bewerking verboden.",
"common.jsNotAllowed": "Script heeft de bewerking verboden.",
"common.jsParseError": "Kan script niet uitvoeren met Javascript-syntaxisfout: {message}",
"common.jsRejected": "Script heeft de bewerking afgewezen.",
"common.language": "Taalcode",

10
backend/i18n/source/frontend_en.json

@ -550,6 +550,16 @@
"roles.deleteConfirmTitle": "Do you really want to delete the role?",
"roles.loadFailed": "Failed to load roles. Please reload.",
"roles.loadPermissionsFailed": "Failed to load permissions. Please reload.",
"roles.permissions": "Permissions",
"roles.permissionsDescription": "Permissions restrict the allowed operations and queries at API level and are a security feature.",
"roles.permissionsPlaceholder": "Start typing to search for permissions",
"roles.properties": "Properties",
"roles.properties.hideAPI": "Hide API",
"roles.properties.hideAssets": "Hide Assets",
"roles.properties.hideContents": "Hide {schema} Contents",
"roles.properties.hideSchemas": "Hide Schemas",
"roles.properties.hideSettings": "Hide Settings",
"roles.propertiesDescription": "Properties describe the behavior of the Management UI, but do not provide security for the API.",
"roles.refreshTooltip": "Refresh roles (CTRL + SHIFT + R)",
"roles.reloaded": "Roles reloaded.",
"roles.revokeFailed": "Failed to revoke role. Please reload.",

2
backend/src/Migrations/OldEvents/SchemaCreated.cs

@ -52,7 +52,7 @@ namespace Migrations.OldEvents
var field = eventField.Properties.CreateRootField(totalFields, eventField.Name, partitioning);
if (field is ArrayField arrayField && eventField.Nested?.Count > 0)
if (field is ArrayField arrayField && eventField.Nested?.Length > 0)
{
foreach (var nestedEventField in eventField.Nested)
{

11
backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs

@ -36,16 +36,21 @@ namespace Squidex.Domain.Apps.Core.Apps
Role = role;
ApiCallsLimit = apiCallsLimit;
ApiTrafficLimit = apiTrafficLimit;
AllowAnonymous = allowAnonymous;
}
[Pure]
public AppClient Update(string? name, string? role, long? apiCallsLimit, long? apiTrafficLimit, bool? allowAnonymous)
public AppClient Update(string? name, string? role,
long? apiCallsLimit,
long? apiTrafficLimit,
bool? allowAnonymous)
{
return new AppClient(name.Or(Name), Secret, role.Or(Role), apiCallsLimit ?? ApiCallsLimit, apiTrafficLimit ?? ApiTrafficLimit, allowAnonymous ?? AllowAnonymous);
return new AppClient(name.Or(Name), Secret, role.Or(Role),
apiCallsLimit ?? ApiCallsLimit,
apiTrafficLimit ?? ApiTrafficLimit,
allowAnonymous ?? AllowAnonymous);
}
}
}

13
backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs

@ -53,11 +53,16 @@ namespace Squidex.Domain.Apps.Core.Apps
throw new ArgumentException("Id already exists.", nameof(id));
}
return Add(id, new AppClient(id, secret, Role.Editor));
var newClient = new AppClient(id, secret, Role.Editor);
return Add(id, newClient);
}
[Pure]
public AppClients Update(string id, string? name = null, string? role = null, long? apiCallsLimit = null, long? apiTrafficLimit = null, bool? allowAnonymous = false)
public AppClients Update(string id, string? name = null, string? role = null,
long? apiCallsLimit = null,
long? apiTrafficLimit = null,
bool? allowAnonymous = false)
{
Guard.NotNullOrEmpty(id, nameof(id));
@ -66,7 +71,9 @@ namespace Squidex.Domain.Apps.Core.Apps
return this;
}
return With<AppClients>(id, client.Update(name, role, apiCallsLimit, apiTrafficLimit, allowAnonymous));
var newClient = client.Update(name, role, apiCallsLimit, apiTrafficLimit, allowAnonymous);
return With<AppClients>(id, newClient);
}
}
}

4
backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs

@ -47,7 +47,9 @@ namespace Squidex.Domain.Apps.Core.Apps
return this;
}
return With<AppPatterns>(id, appPattern.Update(name, pattern, message));
var newPattern = appPattern.Update(name, pattern, message);
return With<AppPatterns>(id, newPattern);
}
}
}

21
backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonRole.cs

@ -0,0 +1,21 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Newtonsoft.Json;
using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Core.Apps.Json
{
public sealed class JsonRole
{
[JsonProperty]
public string[] Permissions { get; set; }
[JsonProperty]
public JsonObject Properties { get; set; }
}
}

72
backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/RoleConverter.cs

@ -0,0 +1,72 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Newtonsoft.Json;
using Squidex.Infrastructure.Json.Newtonsoft;
using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Core.Apps.Json
{
public sealed class RoleConverter : JsonClassConverter<JsonRole>
{
protected override void WriteValue(JsonWriter writer, JsonRole value, JsonSerializer serializer)
{
writer.WriteStartObject();
writer.WritePropertyName("permissions");
serializer.Serialize(writer, value.Permissions);
writer.WritePropertyName("properties");
serializer.Serialize(writer, value.Properties);
writer.WriteEndObject();
}
protected override JsonRole ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer)
{
var permissions = Array.Empty<string>();
var properties = (JsonObject?)null;
if (reader.TokenType == JsonToken.StartArray)
{
permissions = serializer.Deserialize<string[]>(reader)!;
}
else
{
while (reader.Read() && reader.TokenType != JsonToken.EndObject)
{
if (reader.TokenType == JsonToken.PropertyName)
{
var propertyName = reader.Value!.ToString()!;
if (!reader.Read())
{
throw new JsonSerializationException("Unexpected end when reading role.");
}
switch (propertyName.ToLowerInvariant())
{
case "permissions":
permissions = serializer.Deserialize<string[]>(reader)!;
break;
case "properties":
properties = serializer.Deserialize<JsonObject>(reader)!;
break;
}
}
}
}
return new JsonRole
{
Permissions = permissions,
Properties = properties ?? JsonValue.Object()
};
}
}
}

22
backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/RolesConverter.cs

@ -18,11 +18,15 @@ namespace Squidex.Domain.Apps.Core.Apps.Json
{
protected override void WriteValue(JsonWriter writer, Roles value, JsonSerializer serializer)
{
var json = new Dictionary<string, string[]>(value.CustomCount);
var json = new Dictionary<string, JsonRole>(value.CustomCount);
foreach (var role in value.Custom)
{
json.Add(role.Name, role.Permissions.ToIds().ToArray());
json.Add(role.Name, new JsonRole
{
Permissions = role.Permissions.ToIds().ToArray(),
Properties = role.Properties
});
}
serializer.Serialize(writer, json);
@ -30,14 +34,24 @@ namespace Squidex.Domain.Apps.Core.Apps.Json
protected override Roles ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer)
{
var json = serializer.Deserialize<Dictionary<string, string[]>>(reader)!;
var json = serializer.Deserialize<Dictionary<string, JsonRole>>(reader)!;
if (json.Count == 0)
{
return Roles.Empty;
}
return new Roles(json.ToDictionary(x => x.Key, x => new Role(x.Key, new PermissionSet(x.Value))));
return new Roles(json.ToDictionary(x => x.Key, x =>
{
var permissions = PermissionSet.Empty;
if (x.Value.Permissions.Length > 0)
{
permissions = new PermissionSet(x.Value.Permissions);
}
return new Role(x.Key, permissions, x.Value.Properties);
}));
}
}
}

65
backend/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs

@ -7,9 +7,11 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.Contracts;
using System.Linq;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Security;
using P = Squidex.Shared.Permissions;
@ -18,36 +20,68 @@ namespace Squidex.Domain.Apps.Core.Apps
[Equals(DoNotAddEqualityOperators = true)]
public sealed class Role : Named
{
private static readonly HashSet<string> ExtraPermissions = new HashSet<string>
{
P.AppComments,
P.AppContributorsRead,
P.AppHistory,
P.AppHistory,
P.AppLanguagesRead,
P.AppPatternsRead,
P.AppPing,
P.AppRolesRead,
P.AppSchemasRead,
P.AppSearch,
P.AppTranslate,
P.AppUsage
};
public const string Editor = "Editor";
public const string Developer = "Developer";
public const string Owner = "Owner";
public const string Reader = "Reader";
public static readonly ReadOnlyCollection<string> EmptyProperties = new ReadOnlyCollection<string>(new List<string>());
public PermissionSet Permissions { get; }
public JsonObject Properties { get; }
[IgnoreDuringEquals]
public bool IsDefault
{
get { return Roles.IsDefault(this); }
}
public Role(string name, PermissionSet permissions)
public Role(string name, PermissionSet permissions, JsonObject properties)
: base(name)
{
Guard.NotNull(permissions, nameof(permissions));
Guard.NotNull(properties, nameof(properties));
Permissions = permissions;
Properties = properties;
}
public static Role WithPermissions(string role, params string[] permissions)
{
return new Role(role, new PermissionSet(permissions), JsonValue.Object());
}
public static Role WithProperties(string role, JsonObject properties)
{
return new Role(role, PermissionSet.Empty, properties);
}
public Role(string name, params string[] permissions)
: this(name, new PermissionSet(permissions))
public static Role Create(string role)
{
return new Role(role, PermissionSet.Empty, JsonValue.Object());
}
[Pure]
public Role Update(string[] permissions)
public Role Update(PermissionSet? permissions, JsonObject? properties)
{
return new Role(Name, new PermissionSet(permissions));
return new Role(Name, permissions ?? Permissions, properties ?? Properties);
}
public bool Equals(string name)
@ -55,12 +89,11 @@ namespace Squidex.Domain.Apps.Core.Apps
return name != null && name.Equals(Name, StringComparison.Ordinal);
}
public Role ForApp(string app)
public Role ForApp(string app, bool isFrontend = false)
{
var result = new HashSet<Permission>
{
P.ForApp(P.AppCommon, app)
};
Guard.NotNullOrEmpty(app, nameof(app));
var result = new HashSet<Permission>();
if (Permissions.Any())
{
@ -72,7 +105,17 @@ namespace Squidex.Domain.Apps.Core.Apps
}
}
return new Role(Name, new PermissionSet(result));
if (isFrontend)
{
foreach (var extraPermissionId in ExtraPermissions)
{
var extraPermission = P.ForApp(extraPermissionId, app);
result.Add(extraPermission);
}
}
return new Role(Name, new PermissionSet(result), Properties);
}
}
}

76
backend/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs

@ -12,6 +12,7 @@ using System.Diagnostics.Contracts;
using System.Linq;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Collections;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Security;
using Squidex.Shared;
@ -24,28 +25,37 @@ namespace Squidex.Domain.Apps.Core.Apps
public static readonly IReadOnlyDictionary<string, Role> Defaults = new Dictionary<string, Role>
{
[Role.Owner] =
new Role(Role.Owner, new PermissionSet(
Clean(Permissions.App))),
new Role(Role.Owner,
new PermissionSet(
Clean(Permissions.App)),
JsonValue.Object()),
[Role.Reader] =
new Role(Role.Reader, new PermissionSet(
Clean(Permissions.AppAssetsRead),
Clean(Permissions.AppContentsRead))),
new Role(Role.Reader,
new PermissionSet(
Clean(Permissions.AppAssetsRead),
Clean(Permissions.AppContentsRead)),
JsonValue.Object()
.Add("ui.api.hide", true)),
[Role.Editor] =
new Role(Role.Editor, new PermissionSet(
Clean(Permissions.AppAssets),
Clean(Permissions.AppContents),
Clean(Permissions.AppRolesRead),
Clean(Permissions.AppWorkflowsRead))),
new Role(Role.Editor,
new PermissionSet(
Clean(Permissions.AppAssets),
Clean(Permissions.AppContents),
Clean(Permissions.AppRolesRead),
Clean(Permissions.AppWorkflowsRead)),
JsonValue.Object()
.Add("ui.api.hide", true)),
[Role.Developer] =
new Role(Role.Developer, new PermissionSet(
Clean(Permissions.AppApi),
Clean(Permissions.AppAssets),
Clean(Permissions.AppContents),
Clean(Permissions.AppPatterns),
Clean(Permissions.AppRolesRead),
Clean(Permissions.AppRules),
Clean(Permissions.AppSchemas),
Clean(Permissions.AppWorkflows)))
new Role(Role.Developer,
new PermissionSet(
Clean(Permissions.AppAssets),
Clean(Permissions.AppContents),
Clean(Permissions.AppPatterns),
Clean(Permissions.AppRolesRead),
Clean(Permissions.AppRules),
Clean(Permissions.AppSchemas),
Clean(Permissions.AppWorkflows)),
JsonValue.Object())
};
public static readonly Roles Empty = new Roles(new ImmutableDictionary<string, Role>());
@ -89,8 +99,6 @@ namespace Squidex.Domain.Apps.Core.Apps
[Pure]
public Roles Add(string name)
{
var newRole = new Role(name);
if (inner.ContainsKey(name))
{
return this;
@ -101,21 +109,24 @@ namespace Squidex.Domain.Apps.Core.Apps
return this;
}
var newRole = Role.Create(name);
return Create(inner.With(name, newRole));
}
[Pure]
public Roles Update(string name, params string[] permissions)
public Roles Update(string name, PermissionSet? permissions = null, JsonObject? properties = null)
{
Guard.NotNullOrEmpty(name, nameof(name));
Guard.NotNull(permissions, nameof(permissions));
if (!inner.TryGetValue(name, out var role))
{
return this;
}
return Create(inner.With(name, role.Update(permissions)));
var newRole = role.Update(permissions, properties);
return Create(inner.With(name, newRole));
}
public static bool IsDefault(string role)
@ -138,19 +149,22 @@ namespace Squidex.Domain.Apps.Core.Apps
return inner.ContainsKey(name) || Defaults.ContainsKey(name);
}
public bool TryGet(string app, string name, [MaybeNullWhen(false)] out Role value)
public bool TryGet(string app, string name, bool isFrontend, [MaybeNullWhen(false)] out Role value)
{
Guard.NotNull(app, nameof(app));
if (Defaults.TryGetValue(name, out var role) || inner.TryGetValue(name, out role))
value = null!;
if (Defaults.TryGetValue(name, out var role))
{
value = role.ForApp(app);
return true;
value = role.ForApp(app, isFrontend && name != Role.Owner);
}
else if (inner.TryGetValue(name, out role))
{
value = role.ForApp(app, isFrontend);
}
value = null!;
return false;
return value != null;
}
private static string Clean(string permission)

2
backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs

@ -213,7 +213,7 @@ namespace Squidex.Domain.Apps.Core.EventSynchronization
if (sourceNames.SetEquals(targetNames) && !sourceNames.SequenceEqual(targetNames))
{
var fieldIds = targetNames.Select(x => sourceIds.Find(y => y.Name == x)!.Id).ToList();
var fieldIds = targetNames.Select(x => sourceIds.Find(y => y.Name == x)!.Id).ToArray();
yield return new SchemaFieldsReordered { FieldIds = fieldIds, ParentFieldId = parentId };
}

10
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs

@ -30,9 +30,9 @@ namespace Squidex.Domain.Apps.Core.Scripting
private readonly IJintExtension[] extensions;
private readonly Parser parser;
public TimeSpan Timeout { get; set; } = TimeSpan.FromMilliseconds(200);
public TimeSpan TimeoutScript { get; set; } = TimeSpan.FromMilliseconds(200);
public TimeSpan ExecutionTimeout { get; set; } = TimeSpan.FromMilliseconds(4000);
public TimeSpan TimeoutExecution { get; set; } = TimeSpan.FromMilliseconds(4000);
public JintScriptEngine(IMemoryCache memoryCache, IEnumerable<IJintExtension>? extensions = null)
{
@ -46,7 +46,7 @@ namespace Squidex.Domain.Apps.Core.Scripting
Guard.NotNull(vars, nameof(vars));
Guard.NotNullOrEmpty(script, nameof(script));
using (var cts = new CancellationTokenSource(ExecutionTimeout))
using (var cts = new CancellationTokenSource(TimeoutExecution))
{
var tcs = new TaskCompletionSource<IJsonValue>();
@ -76,7 +76,7 @@ namespace Squidex.Domain.Apps.Core.Scripting
Guard.NotNull(vars, nameof(vars));
Guard.NotNullOrEmpty(script, nameof(script));
using (var cts = new CancellationTokenSource(ExecutionTimeout))
using (var cts = new CancellationTokenSource(TimeoutExecution))
{
var tcs = new TaskCompletionSource<NamedContentData>();
@ -140,7 +140,7 @@ namespace Squidex.Domain.Apps.Core.Scripting
options.AddObjectConverter(DefaultConverter.Instance);
options.SetReferencesResolver(NullPropagation.Instance);
options.Strict();
options.TimeoutInterval(Timeout);
options.TimeoutInterval(TimeoutScript);
});
if (options.CanDisallow)

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

@ -18,7 +18,7 @@ namespace Squidex.Domain.Apps.Core.Scripting
private static readonly MessageDelegate Disallow = message =>
{
message = !string.IsNullOrWhiteSpace(message) ? message : T.Get("common.jsNotAlloweed");
message = !string.IsNullOrWhiteSpace(message) ? message : T.Get("common.jsNotAllowed");
throw new DomainForbiddenException(message);
};

2
backend/src/Squidex.Domain.Apps.Entities.MongoDb/FullText/MongoTextIndex.cs

@ -55,7 +55,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.FullText
{
switch (command)
{
case DeleteIndexEntry delete:
case DeleteIndexEntry _:
writes.Add(
new DeleteOneModel<MongoTextIndexEntity>(
Filter.Eq(x => x.DocId, command.DocId)));

11
backend/src/Squidex.Domain.Apps.Entities/Apps/AppSettingsSearchSource.cs

@ -56,19 +56,16 @@ namespace Squidex.Domain.Apps.Entities.Apps
Search("Clients", Permissions.AppClientsRead,
urlGenerator.ClientsUI, SearchResultType.Setting);
Search("Contents", Permissions.AppCommon,
urlGenerator.ContentsUI, SearchResultType.Content);
Search("Contributors", Permissions.AppContributorsRead,
urlGenerator.ContributorsUI, SearchResultType.Setting);
Search("Dashboard", Permissions.AppCommon,
Search("Dashboard", Permissions.AppUsage,
urlGenerator.DashboardUI, SearchResultType.Dashboard);
Search("Languages", Permissions.AppCommon,
Search("Languages", Permissions.AppLanguagesRead,
urlGenerator.LanguagesUI, SearchResultType.Setting);
Search("Patterns", Permissions.AppCommon,
Search("Patterns", Permissions.AppPatternsRead,
urlGenerator.PatternsUI, SearchResultType.Setting);
Search("Roles", Permissions.AppRolesRead,
@ -77,7 +74,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
Search("Rules", Permissions.AppRulesRead,
urlGenerator.RulesUI, SearchResultType.Rule);
Search("Schemas", Permissions.AppCommon,
Search("Schemas", Permissions.AppSchemasRead,
urlGenerator.SchemasUI, SearchResultType.Schema);
Search("Subscription", Permissions.AppPlansRead,

3
backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateLanguage.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Apps.Commands
@ -18,6 +17,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.Commands
public bool IsMaster { get; set; }
public List<Language>? Fallback { get; set; }
public Language[]? Fallback { get; set; }
}
}

4
backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateRole.cs

@ -5,6 +5,8 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Entities.Apps.Commands
{
public sealed class UpdateRole : AppUpdateCommand
@ -12,5 +14,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Commands
public string Name { get; set; }
public string[] Permissions { get; set; }
public JsonObject? Properties { get; set; }
}
}

2
backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppLanguages.cs

@ -81,7 +81,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
e(T.Get("apps.languages.masterLanguageNotOptional"), nameof(command.IsMaster));
}
if (command.Fallback?.Count > 0)
if (command.Fallback?.Length > 0)
{
e(T.Get("apps.languages.masterLanguageNoFallbacks"), nameof(command.Fallback));
}

6
backend/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs

@ -78,7 +78,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.State
return UpdateImage(e, ev => null);
case AppPlanChanged e when Is.Change(Plan?.PlanId, e.PlanId):
return UpdatePlan(e, ev => new AppPlan(ev.Actor, ev.PlanId));
return UpdatePlan(e, ev => ev.ToAppPlan());
case AppPlanReset e when Plan != null:
return UpdatePlan(e, ev => null);
@ -120,7 +120,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.State
return UpdateRoles(e, (ev, r) => r.Add(ev.Name));
case AppRoleUpdated e:
return UpdateRoles(e, (ev, r) => r.Update(ev.Name, ev.Permissions));
return UpdateRoles(e, (ev, r) => r.Update(ev.Name, ev.ToPermissions(), ev.Properties));
case AppRoleDeleted e:
return UpdateRoles(e, (ev, r) => r.Remove(ev.Name));
@ -134,7 +134,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.State
case AppLanguageUpdated e:
return UpdateLanguages(e, (ev, l) =>
{
l = l.Set(ev.Language, ev.IsOptional, ev.Fallback?.ToArray());
l = l.Set(ev.Language, ev.IsOptional, ev.Fallback);
if (ev.IsMaster)
{

12
backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/SchemaBuilder.cs

@ -6,7 +6,7 @@
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Schemas.Commands;
using Squidex.Text;
@ -144,8 +144,14 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates.Builders
}
};
command.Fields ??= new List<UpsertSchemaField>();
command.Fields.Add(field);
if (command.Fields == null)
{
command.Fields = new[] { field };
}
else
{
command.Fields = command.Fields.Union(Enumerable.Repeat(field, 1)).ToArray();
}
return field;
}

6
backend/src/Squidex.Domain.Apps.Entities/Contents/BulkUpdateCommandMiddleware.cs

@ -36,12 +36,12 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
if (context.Command is BulkUpdateContents bulkUpdates)
{
if (bulkUpdates.Jobs?.Count > 0)
if (bulkUpdates.Jobs?.Length > 0)
{
var requestContext = contextProvider.Context.WithoutContentEnrichment().WithUnpublished(true);
var requestedSchema = bulkUpdates.SchemaId.Name;
var results = new BulkUpdateResultItem[bulkUpdates.Jobs.Count];
var results = new BulkUpdateResultItem[bulkUpdates.Jobs.Length];
var actionBlock = new ActionBlock<int>(async index =>
{
@ -115,7 +115,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
MaxDegreeOfParallelism = Math.Max(1, Environment.ProcessorCount / 2)
});
for (var i = 0; i < bulkUpdates.Jobs.Count; i++)
for (var i = 0; i < bulkUpdates.Jobs.Length; i++)
{
await actionBlock.SendAsync(i);
}

3
backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/BulkUpdateContents.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents.Commands
@ -24,6 +23,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Commands
public bool OptimizeValidation { get; set; }
public List<BulkUpdateJob>? Jobs { get; set; }
public BulkUpdateJob[]? Jobs { get; set; }
}
}

2
backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs

@ -114,7 +114,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
CheckErrors(validator);
}
private void CheckErrors(ContentValidator validator)
private static void CheckErrors(ContentValidator validator)
{
if (validator.Errors.Count > 0)
{

2
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs

@ -33,8 +33,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
public ICommandBus CommandBus { get; }
public ISemanticLog Log { get; }
public GraphQLExecutionContext(Context context, IServiceProvider resolver)
: base(context
.WithoutCleanup()

10
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentActions.cs

@ -37,7 +37,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
}
};
public static readonly ValueResolver Resolver = new ValueResolver((value, c) =>
public static readonly ValueResolver Resolver = (value, c) =>
{
if (c.Arguments.TryGetValue("path", out var p) && p is string path)
{
@ -47,7 +47,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
}
return value;
});
};
}
public static readonly QueryArguments JsonPath = new QueryArguments
@ -124,7 +124,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
Description = "Optional OData full text search.",
DefaultValue = string.Empty,
ResolvedType = AllTypes.String
},
}
};
}
@ -152,7 +152,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
Name = "data",
Description = "The data for the content.",
DefaultValue = null,
ResolvedType = new NonNullGraphType(inputType),
ResolvedType = new NonNullGraphType(inputType)
},
new QueryArgument(AllTypes.None)
{
@ -209,7 +209,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
Name = "data",
Description = "The data for the content.",
DefaultValue = null,
ResolvedType = new NonNullGraphType(inputType),
ResolvedType = new NonNullGraphType(inputType)
},
new QueryArgument(AllTypes.None)
{

5
backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureFieldRules.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using Squidex.Domain.Apps.Core.Schemas;
@ -13,11 +12,11 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Commands
{
public sealed class ConfigureFieldRules : SchemaUpdateCommand
{
public List<FieldRuleCommand>? FieldRules { get; set; }
public FieldRuleCommand[]? FieldRules { get; set; }
public FieldRules ToFieldRules()
{
if (FieldRules?.Count > 0)
if (FieldRules?.Length > 0)
{
return new FieldRules(FieldRules.Select(x => x.ToFieldRule()).ToList());
}

7
backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchema.cs

@ -9,8 +9,7 @@ using System.Collections.Generic;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using FieldRules = System.Collections.Generic.List<Squidex.Domain.Apps.Entities.Schemas.Commands.FieldRuleCommand>;
using SchemaFields = System.Collections.Generic.List<Squidex.Domain.Apps.Entities.Schemas.Commands.UpsertSchemaField>;
using SchemaField = Squidex.Domain.Apps.Entities.Schemas.Commands.UpsertSchemaField;
namespace Squidex.Domain.Apps.Entities.Schemas.Commands
{
@ -26,13 +25,13 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Commands
public bool IsSingleton { get; set; }
public SchemaFields Fields { get; set; }
public SchemaField[]? Fields { get; set; }
public FieldNames? FieldsInReferences { get; set; }
public FieldNames? FieldsInLists { get; set; }
public FieldRules? FieldRules { get; set; }
public FieldRuleCommand[]? FieldRules { get; set; }
public SchemaScripts? Scripts { get; set; }

9
backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/IUpsertCommand.cs

@ -9,8 +9,7 @@ using System.Collections.Generic;
using System.Linq;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Schemas;
using FieldRules = System.Collections.Generic.List<Squidex.Domain.Apps.Entities.Schemas.Commands.FieldRuleCommand>;
using SchemaFields = System.Collections.Generic.List<Squidex.Domain.Apps.Entities.Schemas.Commands.UpsertSchemaField>;
using SchemaField = Squidex.Domain.Apps.Entities.Schemas.Commands.UpsertSchemaField;
namespace Squidex.Domain.Apps.Entities.Schemas.Commands
{
@ -20,13 +19,13 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Commands
string Category { get; set; }
SchemaFields Fields { get; set; }
SchemaField[]? Fields { get; set; }
FieldNames? FieldsInReferences { get; set; }
FieldNames? FieldsInLists { get; set; }
FieldRules? FieldRules { get; set; }
FieldRuleCommand[]? FieldRules { get; set; }
SchemaScripts? Scripts { get; set; }
@ -85,7 +84,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Commands
var field = eventField.Properties.CreateRootField(totalFields, eventField.Name, partitioning);
if (field is ArrayField arrayField && eventField.Nested?.Count > 0)
if (field is ArrayField arrayField && eventField.Nested?.Length > 0)
{
foreach (var nestedEventField in eventField.Nested)
{

4
backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ReorderFields.cs

@ -5,12 +5,10 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
namespace Squidex.Domain.Apps.Entities.Schemas.Commands
{
public sealed class ReorderFields : ParentFieldCommand
{
public List<long> FieldIds { get; set; }
public long[] FieldIds { get; set; }
}
}

7
backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/SynchronizeSchema.cs

@ -8,8 +8,7 @@
using System.Collections.Generic;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure.Commands;
using FieldRules = System.Collections.Generic.List<Squidex.Domain.Apps.Entities.Schemas.Commands.FieldRuleCommand>;
using SchemaFields = System.Collections.Generic.List<Squidex.Domain.Apps.Entities.Schemas.Commands.UpsertSchemaField>;
using SchemaField = Squidex.Domain.Apps.Entities.Schemas.Commands.UpsertSchemaField;
namespace Squidex.Domain.Apps.Entities.Schemas.Commands
{
@ -23,13 +22,13 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Commands
public string Category { get; set; }
public SchemaFields Fields { get; set; }
public SchemaField[]? Fields { get; set; }
public FieldNames? FieldsInReferences { get; set; }
public FieldNames? FieldsInLists { get; set; }
public FieldRules? FieldRules { get; set; }
public FieldRuleCommand[]? FieldRules { get; set; }
public SchemaScripts? Scripts { get; set; }

4
backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaField.cs

@ -5,14 +5,12 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
namespace Squidex.Domain.Apps.Entities.Schemas.Commands
{
public sealed class UpsertSchemaField : UpsertSchemaFieldBase
{
public string Partitioning { get; set; }
public List<UpsertSchemaNestedField> Nested { get; set; }
public UpsertSchemaNestedField[]? Nested { get; set; }
}
}

12
backend/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs

@ -143,7 +143,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
private static void ValidateUpsert(IUpsertCommand command, AddValidation e)
{
if (command.Fields?.Count > 0)
if (command.Fields?.Length > 0)
{
command.Fields.Foreach((field, fieldIndex) =>
{
@ -182,7 +182,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
ValidateField(field, prefix, e);
if (field.Nested?.Count > 0)
if (field.Nested?.Length > 0)
{
if (field.Properties is ArrayFieldProperties)
{
@ -193,7 +193,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
ValidateNestedField(nestedField, nestedPrefix, e);
});
}
else if (field.Nested.Count > 0)
else if (field.Nested.Length > 0)
{
e(T.Get("schemas.onlyArraysHaveNested"), $"{prefix}.{nameof(field.Partitioning)}");
}
@ -292,7 +292,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
}
}
private static void ValidateFieldRules(List<FieldRuleCommand>? fieldRules, string path, AddValidation e)
private static void ValidateFieldRules(FieldRuleCommand[]? fieldRules, string path, AddValidation e)
{
fieldRules?.Foreach((rule, ruleIndex) =>
{
@ -318,7 +318,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
{
var fieldPrefix = $"{path}[{fieldIndex}]";
var field = command?.Fields?.Find(x => x.Name == fieldName);
var field = command?.Fields?.FirstOrDefault(x => x.Name == fieldName);
if (string.IsNullOrWhiteSpace(fieldName))
{
@ -356,7 +356,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
private static void ValidateFieldIds<TField>(ReorderFields c, IReadOnlyDictionary<long, TField> fields, AddValidation e)
{
if (c.FieldIds != null && (c.FieldIds.Count != fields.Count || c.FieldIds.Any(x => !fields.ContainsKey(x))))
if (c.FieldIds != null && (c.FieldIds.Length != fields.Count || c.FieldIds.Any(x => !fields.ContainsKey(x))))
{
e(T.Get("schemas.fieldsNotCovered"), nameof(c.FieldIds));
}

4
backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasIndex.cs

@ -211,12 +211,12 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes
return schema;
}
private string GetCacheKey(DomainId appId, string name)
private static string GetCacheKey(DomainId appId, string name)
{
return $"SCHEMAS_NAME_{appId}_{name}";
}
private string GetCacheKey(DomainId appId, DomainId id)
private static string GetCacheKey(DomainId appId, DomainId id)
{
return $"SCHEMAS_ID_{appId}_{id}";
}

11
backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs

@ -59,17 +59,6 @@ namespace Squidex.Domain.Apps.Entities.Schemas
Add(c);
long id;
if (c.ParentFieldId == null)
{
id = Snapshot.SchemaDef.FieldsByName[c.Name].Id;
}
else
{
id = ((IArrayField)Snapshot.SchemaDef.FieldsById[c.ParentFieldId.Value]).FieldsByName[c.Name].Id;
}
return Snapshot;
});

3
backend/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs

@ -6,6 +6,7 @@
// ==========================================================================
using System;
using System.Linq;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Events.Schemas;
@ -138,7 +139,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.State
case SchemaFieldsReordered e:
{
SchemaDef = SchemaDef.ReorderFields(e.FieldIds, e.ParentFieldId?.Id);
SchemaDef = SchemaDef.ReorderFields(e.FieldIds.ToList(), e.ParentFieldId?.Id);
break;
}

3
backend/src/Squidex.Domain.Apps.Events/Apps/AppLanguageUpdated.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Reflection;
@ -20,6 +19,6 @@ namespace Squidex.Domain.Apps.Events.Apps
public bool IsMaster { get; set; }
public List<Language>? Fallback { get; set; }
public Language[]? Fallback { get; set; }
}
}

6
backend/src/Squidex.Domain.Apps.Events/Apps/AppPlanChanged.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Events.Apps
@ -13,5 +14,10 @@ namespace Squidex.Domain.Apps.Events.Apps
public sealed class AppPlanChanged : AppEvent
{
public string PlanId { get; set; }
public AppPlan ToAppPlan()
{
return new AppPlan(Actor, PlanId);
}
}
}

9
backend/src/Squidex.Domain.Apps.Events/Apps/AppRoleUpdated.cs

@ -6,6 +6,8 @@
// ==========================================================================
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Security;
namespace Squidex.Domain.Apps.Events.Apps
{
@ -15,5 +17,12 @@ namespace Squidex.Domain.Apps.Events.Apps
public string Name { get; set; }
public string[] Permissions { get; set; }
public JsonObject? Properties { get; set; }
public PermissionSet ToPermissions()
{
return new PermissionSet(Permissions);
}
}
}

4
backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedField.cs

@ -5,14 +5,12 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
namespace Squidex.Domain.Apps.Events.Schemas
{
public sealed class SchemaCreatedField : SchemaCreatedFieldBase
{
public string Partitioning { get; set; }
public List<SchemaCreatedNestedField> Nested { get; set; }
public SchemaCreatedNestedField[] Nested { get; set; }
}
}

3
backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaFieldsReordered.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Events.Schemas
@ -13,6 +12,6 @@ namespace Squidex.Domain.Apps.Events.Schemas
[EventType(nameof(SchemaFieldsReordered))]
public sealed class SchemaFieldsReordered : ParentFieldEvent
{
public List<long> FieldIds { get; set; }
public long[] FieldIds { get; set; }
}
}

4
backend/src/Squidex.Infrastructure.MongoDb/Assets/MongoGridFsAssetStore.cs

@ -49,9 +49,9 @@ namespace Squidex.Infrastructure.Assets
{
var name = GetFileName(fileName, nameof(fileName));
var find = await bucket.FindAsync(Builders<GridFSFileInfo<string>>.Filter.Eq(x => x.Id, name), cancellationToken: ct);
var query = await bucket.FindAsync(Builders<GridFSFileInfo<string>>.Filter.Eq(x => x.Id, name), cancellationToken: ct);
var file = await find.FirstOrDefaultAsync(ct);
var file = await query.FirstOrDefaultAsync(cancellationToken: ct);
if (file == null)
{

18
backend/src/Squidex.Infrastructure/Assets/ImageFormat.cs

@ -0,0 +1,18 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Infrastructure.Assets
{
public enum ImageFormat
{
Auto,
PNG,
JPEG,
TGA,
GIF
}
}

2
backend/src/Squidex.Infrastructure/Assets/ImageInfo.cs

@ -9,6 +9,8 @@ namespace Squidex.Infrastructure.Assets
{
public sealed class ImageInfo
{
public string? Format { get; set; }
public int PixelWidth { get; }
public int PixelHeight { get; }

57
backend/src/Squidex.Infrastructure/Assets/ImageSharp/ImageSharpAssetThumbnailGenerator.cs

@ -10,7 +10,11 @@ using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Formats.Tga;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Processing;
using ISResizeMode = SixLabors.ImageSharp.Processing.ResizeMode;
@ -30,7 +34,7 @@ namespace Squidex.Infrastructure.Assets.ImageSharp
if (!options.IsValid)
{
source.CopyTo(destination);
await source.CopyToAsync(destination);
return;
}
@ -44,17 +48,7 @@ namespace Squidex.Infrastructure.Assets.ImageSharp
{
using (var image = Image.Load(source, out var format))
{
var encoder = Configuration.Default.ImageFormatsManager.FindEncoder(format);
if (encoder == null)
{
throw new NotSupportedException();
}
if (options.Quality.HasValue && (encoder is JpegEncoder || !options.KeepFormat))
{
encoder = new JpegEncoder { Quality = options.Quality.Value };
}
var encoder = GetEncoder(options, format);
image.Mutate(x => x.AutoOrient());
@ -90,7 +84,7 @@ namespace Squidex.Infrastructure.Assets.ImageSharp
image.Mutate(x => x.Resize(resizeOptions));
}
image.Save(destination, encoder);
await image.SaveAsync(destination, encoder);
}
}
finally
@ -99,6 +93,39 @@ namespace Squidex.Infrastructure.Assets.ImageSharp
}
}
private static IImageEncoder GetEncoder(ResizeOptions options, IImageFormat? format)
{
var encoder = Configuration.Default.ImageFormatsManager.FindEncoder(format);
if (encoder == null)
{
throw new NotSupportedException();
}
if (options.Quality.HasValue && (encoder is JpegEncoder || !options.KeepFormat) && options.Format == ImageFormat.Auto)
{
encoder = new JpegEncoder { Quality = options.Quality.Value };
}
else if (options.Format == ImageFormat.JPEG)
{
encoder = new JpegEncoder();
}
else if (options.Format == ImageFormat.PNG)
{
encoder = new PngEncoder();
}
else if (options.Format == ImageFormat.TGA)
{
encoder = new TgaEncoder();
}
else if (options.Format == ImageFormat.GIF)
{
encoder = new GifEncoder();
}
return encoder;
}
public Task<ImageInfo?> GetImageInfoAsync(Stream source)
{
Guard.NotNull(source, nameof(source));
@ -107,11 +134,13 @@ namespace Squidex.Infrastructure.Assets.ImageSharp
try
{
var image = Image.Identify(source);
var image = Image.Identify(source, out var format);
if (image != null)
{
result = GetImageInfo(image);
result.Format = format.Name;
}
}
catch

10
backend/src/Squidex.Infrastructure/Assets/ResizeOptions.cs

@ -11,6 +11,8 @@ namespace Squidex.Infrastructure.Assets
{
public sealed class ResizeOptions
{
public ImageFormat Format { get; set; }
public ResizeMode Mode { get; set; }
public int? Width { get; set; }
@ -27,7 +29,7 @@ namespace Squidex.Infrastructure.Assets
public bool IsValid
{
get { return Width > 0 || Height > 0 || Quality > 0; }
get { return Width > 0 || Height > 0 || Quality > 0 || Format != ImageFormat.Auto; }
}
public override string ToString()
@ -58,6 +60,12 @@ namespace Squidex.Infrastructure.Assets
sb.Append(FocusY);
}
if (Format != ImageFormat.Auto)
{
sb.Append("_format_");
sb.Append(Format.ToString());
}
return sb.ToString();
}
}

12
backend/src/Squidex.Infrastructure/EventSourcing/Grains/BatchSubscriber.cs

@ -22,7 +22,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
private readonly IEventSubscription eventSubscription;
private readonly IDataflowBlock pipelineEnd;
public object Sender
public object? Sender
{
get { return eventSubscription.Sender!; }
}
@ -86,21 +86,21 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
var handle = new ActionBlock<IList<Job>>(async jobs =>
{
var sender = eventSubscription.Sender;
foreach (var jobsBySender in jobs.GroupBy<Job, object>(x => x.Sender))
{
var sender = jobsBySender.Key;
if (ReferenceEquals(sender, eventSubscription.Sender))
if (sender != null && ReferenceEquals(jobsBySender.Key, sender))
{
var exception = jobs.FirstOrDefault(x => x.Exception != null)?.Exception;
if (exception != null)
{
await grain.OnErrorAsync(Sender, exception);
await grain.OnErrorAsync(sender, exception);
}
else
{
await grain.OnEventsAsync(Sender, GetEvents(jobsBySender), GetPosition(jobsBySender));
await grain.OnEventsAsync(sender, GetEvents(jobsBySender), GetPosition(jobsBySender));
}
}
}

2
backend/src/Squidex.Infrastructure/Log/ISemanticLog.cs

@ -11,7 +11,7 @@ namespace Squidex.Infrastructure.Log
{
public delegate void LogFormatter(IObjectWriter writer);
public delegate void LogFormatter<T>(T context, IObjectWriter writer);
public delegate void LogFormatter<in T>(T context, IObjectWriter writer);
public interface ISemanticLog
{

6
backend/src/Squidex.Infrastructure/Tasks/AsyncHelper.cs

@ -60,12 +60,12 @@ namespace Squidex.Infrastructure.Tasks
var timer = new Timer(_ => batchBlock.TriggerBatch());
var timerBlock = new TransformBlock<T, T>((T value) =>
var timerBlock = new TransformBlock<T, T>(value =>
{
timer.Change(timeout, Timeout.Infinite);
return value;
}, new ExecutionDataflowBlockOptions()
}, new ExecutionDataflowBlockOptions
{
BoundedCapacity = 1,
CancellationToken = dataflowBlockOptions.CancellationToken,
@ -76,7 +76,7 @@ namespace Squidex.Infrastructure.Tasks
TaskScheduler = dataflowBlockOptions.TaskScheduler
});
timerBlock.LinkTo(batchBlock, new DataflowLinkOptions()
timerBlock.LinkTo(batchBlock, new DataflowLinkOptions
{
PropagateCompletion = true
});

22
backend/src/Squidex.Shared/Permissions.cs

@ -50,13 +50,28 @@ namespace Squidex.Shared
public const string AdminUsersLock = "squidex.admin.users.lock";
public const string App = "squidex.apps.{app}";
public const string AppCommon = "squidex.apps.{app}.common";
public const string AppDelete = "squidex.apps.{app}.delete";
public const string AppUpdate = "squidex.apps.{app}.update";
public const string AppUpdateImage = "squidex.apps.{app}.update";
public const string AppUpdateGeneral = "squidex.apps.{app}.general";
public const string AppHistory = "squidex.apps.{app}.history";
public const string AppPing = "squidex.apps.{app}.ping";
public const string AppSearch = "squidex.apps.{app}.search";
public const string AppTranslate = "squidex.apps.{app}.translate";
public const string AppUsage = "squidex.apps.{app}.usage";
public const string AppComments = "squidex.apps.{app}.comments";
public const string AppCommentsRead = "squidex.apps.{app}.comments.read";
public const string AppCommentsCreate = "squidex.apps.{app}.comments.create";
public const string AppCommentsUpdate = "squidex.apps.{app}.comments.update";
public const string AppCommentsDelete = "squidex.apps.{app}.comments.delete";
public const string AppClients = "squidex.apps.{app}.clients";
public const string AppClientsRead = "squidex.apps.{app}.clients.read";
public const string AppClientsCreate = "squidex.apps.{app}.clients.create";
@ -69,6 +84,7 @@ namespace Squidex.Shared
public const string AppContributorsRevoke = "squidex.apps.{app}.contributors.revoke";
public const string AppLanguages = "squidex.apps.{app}.languages";
public const string AppLanguagesRead = "squidex.apps.{app}.languages.read";
public const string AppLanguagesCreate = "squidex.apps.{app}.languages.create";
public const string AppLanguagesUpdate = "squidex.apps.{app}.languages.update";
public const string AppLanguagesDelete = "squidex.apps.{app}.languages.delete";
@ -80,6 +96,7 @@ namespace Squidex.Shared
public const string AppRolesDelete = "squidex.apps.{app}.roles.delete";
public const string AppPatterns = "squidex.apps.{app}.patterns";
public const string AppPatternsRead = "squidex.apps.{app}.patterns.read";
public const string AppPatternsCreate = "squidex.apps.{app}.patterns.create";
public const string AppPatternsUpdate = "squidex.apps.{app}.patterns.update";
public const string AppPatternsDelete = "squidex.apps.{app}.patterns.delete";
@ -115,6 +132,7 @@ namespace Squidex.Shared
public const string AppRulesDelete = "squidex.apps.{app}.rules.delete";
public const string AppSchemas = "squidex.apps.{app}.schemas";
public const string AppSchemasRead = "squidex.apps.{app}.schemas.read";
public const string AppSchemasCreate = "squidex.apps.{app}.schemas.create";
public const string AppSchemasUpdate = "squidex.apps.{app}.schemas.{name}.update";
public const string AppSchemasScripts = "squidex.apps.{app}.schemas.{name}.scripts";
@ -130,8 +148,6 @@ namespace Squidex.Shared
public const string AppContentsVersionDelete = "squidex.apps.{app}.contents.{name}.version.delete";
public const string AppContentsDelete = "squidex.apps.{app}.contents.{name}.delete";
public const string AppApi = "squidex.apps.{app}.api";
static Permissions()
{
foreach (var field in typeof(Permissions).GetFields(BindingFlags.Public | BindingFlags.Static))

2
backend/src/Squidex.Shared/Texts.it.resx

@ -283,7 +283,7 @@
<data name="common.jsError" xml:space="preserve">
<value>Esecuzione dello script fallita, Errore Javascript: {message}</value>
</data>
<data name="common.jsNotAlloweed" xml:space="preserve">
<data name="common.jsNotAllowed" xml:space="preserve">
<value>Uno script ha proibito l'operazione.</value>
</data>
<data name="common.jsParseError" xml:space="preserve">

2
backend/src/Squidex.Shared/Texts.nl.resx

@ -283,7 +283,7 @@
<data name="common.jsError" xml:space="preserve">
<value>Kan script niet uitvoeren met Javascript-fout: {message}</value>
</data>
<data name="common.jsNotAlloweed" xml:space="preserve">
<data name="common.jsNotAllowed" xml:space="preserve">
<value>Script heeft de bewerking verboden.</value>
</data>
<data name="common.jsParseError" xml:space="preserve">

2
backend/src/Squidex.Shared/Texts.resx

@ -283,7 +283,7 @@
<data name="common.jsError" xml:space="preserve">
<value>Failed to execute script with Javascript error: {message}</value>
</data>
<data name="common.jsNotAlloweed" xml:space="preserve">
<data name="common.jsNotAllowed" xml:space="preserve">
<value>Script has forbidden the operation.</value>
</data>
<data name="common.jsParseError" xml:space="preserve">

22
backend/src/Squidex.Web/Pipeline/AppResolver.cs

@ -41,9 +41,9 @@ namespace Squidex.Web.Pipeline
if (!string.IsNullOrWhiteSpace(appName))
{
var canCache = !user.IsInClient(DefaultClients.Frontend);
var isFrontend = user.IsInClient(DefaultClients.Frontend);
var app = await appProvider.GetAppAsync(appName, canCache);
var app = await appProvider.GetAppAsync(appName, !isFrontend);
if (app == null)
{
@ -51,16 +51,16 @@ namespace Squidex.Web.Pipeline
return;
}
var (role, permissions) = FindByOpenIdSubject(app, user);
var (role, permissions) = FindByOpenIdSubject(app, user, isFrontend);
if (permissions == null)
{
(role, permissions) = FindByOpenIdClient(app, user);
(role, permissions) = FindByOpenIdClient(app, user, isFrontend);
}
if (permissions == null)
{
(role, permissions) = FindAnonymousClient(app);
(role, permissions) = FindAnonymousClient(app, isFrontend);
}
if (permissions != null)
@ -132,7 +132,7 @@ namespace Squidex.Web.Pipeline
return context.ActionDescriptor.EndpointMetadata.Any(x => x is AllowAnonymousAttribute);
}
private static (string?, PermissionSet?) FindByOpenIdClient(IAppEntity app, ClaimsPrincipal user)
private static (string?, PermissionSet?) FindByOpenIdClient(IAppEntity app, ClaimsPrincipal user, bool isFrontend)
{
var (appName, clientId) = user.GetClient();
@ -141,7 +141,7 @@ namespace Squidex.Web.Pipeline
return (null, null);
}
if (clientId != null && app.Clients.TryGetValue(clientId, out var client) && app.Roles.TryGet(app.Name, client.Role, out var role))
if (clientId != null && app.Clients.TryGetValue(clientId, out var client) && app.Roles.TryGet(app.Name, client.Role, isFrontend, out var role))
{
return (client.Role, role.Permissions);
}
@ -149,11 +149,11 @@ namespace Squidex.Web.Pipeline
return (null, null);
}
private static (string?, PermissionSet?) FindAnonymousClient(IAppEntity app)
private static (string?, PermissionSet?) FindAnonymousClient(IAppEntity app, bool isFrontend)
{
var client = app.Clients.Values.FirstOrDefault(x => x.AllowAnonymous);
if (client != null && app.Roles.TryGet(app.Name, client.Role, out var role))
if (client != null && app.Roles.TryGet(app.Name, client.Role, isFrontend, out var role))
{
return (client.Role, role.Permissions);
}
@ -161,11 +161,11 @@ namespace Squidex.Web.Pipeline
return (null, null);
}
private static (string?, PermissionSet?) FindByOpenIdSubject(IAppEntity app, ClaimsPrincipal user)
private static (string?, PermissionSet?) FindByOpenIdSubject(IAppEntity app, ClaimsPrincipal user, bool isFrontend)
{
var subjectId = user.OpenIdSubject();
if (subjectId != null && app.Contributors.TryGetValue(subjectId, out var roleName) && app.Roles.TryGet(app.Name, roleName, out var role))
if (subjectId != null && app.Contributors.TryGetValue(subjectId, out var roleName) && app.Roles.TryGet(app.Name, roleName, isFrontend, out var role))
{
return (roleName, role.Permissions);
}

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

@ -17,6 +17,7 @@ using Squidex.Areas.Api.Controllers.Rules.Models;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Areas.Api.Config.OpenApi
{
@ -77,18 +78,24 @@ namespace Squidex.Areas.Api.Config.OpenApi
CreateStringMap<RefToken>(),
CreateStringMap<Status>(),
new PrimitiveTypeMapper(typeof(AssetMetadata), schema =>
{
schema.Type = JsonObjectType.Object;
schema.AdditionalPropertiesSchema = new JsonSchema
{
Description = "Any JSON type"
};
})
CreateObjectMap<JsonObject>(),
CreateObjectMap<AssetMetadata>()
};
}
private static ITypeMapper CreateObjectMap<T>()
{
return new PrimitiveTypeMapper(typeof(T), schema =>
{
schema.Type = JsonObjectType.Object;
schema.AdditionalPropertiesSchema = new JsonSchema
{
Description = "Any JSON type"
};
});
}
private static ITypeMapper CreateStringMap<T>(string? format = null)
{
return new PrimitiveTypeMapper(typeof(T), schema =>

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

@ -48,7 +48,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
[HttpGet]
[Route("apps/{app}/contributors/")]
[ProducesResponseType(typeof(ContributorsDto), 200)]
[ApiPermissionOrAnonymous(Permissions.AppCommon)]
[ApiPermissionOrAnonymous(Permissions.AppContributorsRead)]
[ApiCosts(0)]
public IActionResult GetContributors(string app)
{

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

@ -41,7 +41,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
[HttpGet]
[Route("apps/{app}/languages/")]
[ProducesResponseType(typeof(AppLanguagesDto), 200)]
[ApiPermissionOrAnonymous(Permissions.AppCommon)]
[ApiPermissionOrAnonymous(Permissions.AppLanguagesRead)]
[ApiCosts(0)]
public IActionResult GetLanguages(string app)
{

2
backend/src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs

@ -42,7 +42,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
[HttpGet]
[Route("apps/{app}/patterns/")]
[ProducesResponseType(typeof(PatternsDto), 200)]
[ApiPermissionOrAnonymous(Permissions.AppCommon)]
[ApiPermissionOrAnonymous(Permissions.AppPatternsRead)]
[ApiCosts(0)]
public IActionResult GetPatterns(string app)
{

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

@ -80,7 +80,9 @@ namespace Squidex.Areas.Api.Controllers.Apps
var response = Deferred.Response(() =>
{
return apps.OrderBy(x => x.Name).Select(a => AppDto.FromApp(a, userOrClientId, appPlansProvider, Resources)).ToArray();
var isFrontend = HttpContext.User.IsInClient(DefaultClients.Frontend);
return apps.OrderBy(x => x.Name).Select(a => AppDto.FromApp(a, userOrClientId, isFrontend, appPlansProvider, Resources)).ToArray();
});
Response.Headers[HeaderNames.ETag] = apps.ToEtag();
@ -107,7 +109,9 @@ namespace Squidex.Areas.Api.Controllers.Apps
{
var userOrClientId = HttpContext.User.UserOrClientId()!;
return AppDto.FromApp(App, userOrClientId, appPlansProvider, Resources);
var isFrontend = HttpContext.User.IsInClient(DefaultClients.Frontend);
return AppDto.FromApp(App, userOrClientId, isFrontend, appPlansProvider, Resources);
});
Response.Headers[HeaderNames.ETag] = App.ToEtag();
@ -212,7 +216,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
try
{
await assetStore.DownloadAsync(resizedAsset, body);
await assetStore.DownloadAsync(resizedAsset, body, ct: ct);
}
catch (AssetNotFoundException)
{
@ -298,8 +302,10 @@ namespace Squidex.Areas.Api.Controllers.Apps
var userOrClientId = HttpContext.User.UserOrClientId()!;
var isFrontend = HttpContext.User.IsInClient(DefaultClients.Frontend);
var result = context.Result<IAppEntity>();
var response = AppDto.FromApp(result, userOrClientId, appPlansProvider, Resources);
var response = AppDto.FromApp(result, userOrClientId, isFrontend, appPlansProvider, Resources);
return response;
}

37
backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using NodaTime;
using Squidex.Areas.Api.Controllers.Assets;
@ -16,6 +17,7 @@ using Squidex.Areas.Api.Controllers.Schemas;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.Plans;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.Security;
using Squidex.Infrastructure.Validation;
@ -73,6 +75,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// <summary>
/// Indicates if the user can access the api.
/// </summary>
[Obsolete("Usage role properties")]
public bool CanAccessApi { get; set; }
/// <summary>
@ -90,17 +93,30 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// </summary>
public string? PlanUpgrade { get; set; }
public static AppDto FromApp(IAppEntity app, string userId, IAppPlansProvider plans, Resources resources)
/// <summary>
/// The properties from the role.
/// </summary>
[LocalizedRequired]
public JsonObject RoleProperties { get; set; }
public static AppDto FromApp(IAppEntity app, string userId, bool isFrontend, IAppPlansProvider plans, Resources resources)
{
var permissions = GetPermissions(app, userId);
var permissions = GetPermissions(app, userId, isFrontend);
var result = SimpleMapper.Map(app, new AppDto());
result.Permissions = permissions.ToIds();
if (resources.Includes(P.ForApp(P.AppApi, app.Name), permissions))
result.SetPlan(app, plans, resources, permissions);
result.SetImage(app, resources);
if (app.Contributors.TryGetValue(userId, out var roleName) && app.Roles.TryGet(app.Name, roleName, isFrontend, out var role))
{
result.RoleProperties = role.Properties;
}
else
{
result.CanAccessApi = true;
result.RoleProperties = JsonValue.Object();
}
if (resources.Includes(P.ForApp(P.AppContents, app.Name), permissions))
@ -108,17 +124,14 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
result.CanAccessContent = true;
}
result.SetPlan(app, plans, resources, permissions);
result.SetImage(app, resources);
return result.CreateLinks(resources, permissions);
}
private static PermissionSet GetPermissions(IAppEntity app, string userId)
private static PermissionSet GetPermissions(IAppEntity app, string userId, bool isFrontend)
{
var permissions = new List<Permission>();
if (app.Contributors.TryGetValue(userId, out var roleName) && app.Roles.TryGet(app.Name, roleName, out var role))
if (app.Contributors.TryGetValue(userId, out var roleName) && app.Roles.TryGet(app.Name, roleName, isFrontend, out var role))
{
permissions.AddRange(role.Permissions);
}
@ -187,12 +200,12 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
AddGetLink("contributors", resources.Url<AppContributorsController>(x => nameof(x.GetContributors), values));
}
if (resources.IsAllowed(P.AppCommon, Name, additional: permissions))
if (resources.IsAllowed(P.AppLanguagesRead, Name, additional: permissions))
{
AddGetLink("languages", resources.Url<AppLanguagesController>(x => nameof(x.GetLanguages), values));
}
if (resources.IsAllowed(P.AppCommon, Name, additional: permissions))
if (resources.IsAllowed(P.AppPatternsRead, Name, additional: permissions))
{
AddGetLink("patterns", resources.Url<AppPatternsController>(x => nameof(x.GetPatterns), values));
}
@ -212,7 +225,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
AddGetLink("rules", resources.Url<RulesController>(x => nameof(x.GetRules), values));
}
if (resources.IsAllowed(P.AppCommon, Name, additional: permissions))
if (resources.IsAllowed(P.AppSchemasRead, Name, additional: permissions))
{
AddGetLink("schemas", resources.Url<SchemasController>(x => nameof(x.GetSchemas), values));
}

15
backend/src/Squidex/Areas/Api/Controllers/Apps/Models/RoleDto.cs

@ -5,10 +5,10 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Validation;
using Squidex.Web;
@ -41,18 +41,23 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// Associated list of permissions.
/// </summary>
[LocalizedRequired]
public IEnumerable<string> Permissions { get; set; }
public string[] Permissions { get; set; }
/// <summary>
/// Associated list of UI properties.
/// </summary>
[LocalizedRequired]
public JsonObject Properties { get; set; }
public static RoleDto FromRole(Role role, IAppEntity app)
{
var permissions = role.Permissions;
var result = new RoleDto
{
Name = role.Name,
NumClients = GetNumClients(role, app),
NumContributors = GetNumContributors(role, app),
Permissions = permissions.ToIds(),
Permissions = role.Permissions.ToIds().ToArray(),
Properties = role.Properties,
IsDefaultRole = role.IsDefault
};

3
backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateLanguageDto.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Reflection;
@ -27,7 +26,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// <summary>
/// Optional fallback languages.
/// </summary>
public List<Language>? Fallback { get; set; }
public Language[]? Fallback { get; set; }
public UpdateLanguage ToCommand(Language language)
{

8
backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateRoleDto.cs

@ -6,6 +6,7 @@
// ==========================================================================
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Validation;
namespace Squidex.Areas.Api.Controllers.Apps.Models
@ -18,9 +19,14 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
[LocalizedRequired]
public string[] Permissions { get; set; }
/// <summary>
/// Associated list of UI properties.
/// </summary>
public JsonObject? Properties { get; set; }
public UpdateRole ToCommand(string name)
{
return new UpdateRole { Name = name, Permissions = Permissions };
return new UpdateRole { Name = name, Permissions = Permissions, Properties = Properties };
}
}
}

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

@ -121,6 +121,8 @@ namespace Squidex.Areas.Api.Controllers.Assets
if (asset.IsProtected && !Resources.CanReadAssets)
{
Response.Headers[HeaderNames.CacheControl] = $"public,max-age=0";
return StatusCode(403);
}

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

@ -87,6 +87,12 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models
[FromQuery(Name = "force")]
public bool ForceResize { get; set; }
/// <summary>
/// The target image format.
/// </summary>
[FromQuery(Name = "format")]
public ImageFormat Format { get; set; }
public ResizeOptions ToResizeOptions(IAssetEntity asset)
{
Guard.NotNull(asset, nameof(asset));

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

@ -48,7 +48,7 @@ namespace Squidex.Areas.Api.Controllers.Comments
[HttpGet]
[Route("apps/{app}/comments/{commentsId}")]
[ProducesResponseType(typeof(CommentsDto), 200)]
[ApiPermissionOrAnonymous(Permissions.AppCommon)]
[ApiPermissionOrAnonymous(Permissions.AppCommentsRead)]
[ApiCosts(0)]
public async Task<IActionResult> GetComments(string app, string commentsId, [FromQuery] long version = EtagVersion.Any)
{
@ -78,7 +78,7 @@ namespace Squidex.Areas.Api.Controllers.Comments
[HttpPost]
[Route("apps/{app}/comments/{commentsId}")]
[ProducesResponseType(typeof(CommentDto), 201)]
[ApiPermissionOrAnonymous(Permissions.AppCommon)]
[ApiPermissionOrAnonymous(Permissions.AppCommentsCreate)]
[ApiCosts(0)]
public async Task<IActionResult> PostComment(string app, string commentsId, [FromBody] UpsertCommentDto request)
{
@ -105,7 +105,7 @@ namespace Squidex.Areas.Api.Controllers.Comments
/// </returns>
[HttpPut]
[Route("apps/{app}/comments/{commentsId}/{commentId}")]
[ApiPermissionOrAnonymous(Permissions.AppCommon)]
[ApiPermissionOrAnonymous(Permissions.AppCommentsUpdate)]
[ApiCosts(0)]
public async Task<IActionResult> PutComment(string app, string commentsId, string commentId, [FromBody] UpsertCommentDto request)
{
@ -126,7 +126,7 @@ namespace Squidex.Areas.Api.Controllers.Comments
/// </returns>
[HttpDelete]
[Route("apps/{app}/comments/{commentsId}/{commentId}")]
[ApiPermissionOrAnonymous(Permissions.AppCommon)]
[ApiPermissionOrAnonymous(Permissions.AppCommentsDelete)]
[ApiCosts(0)]
public async Task<IActionResult> DeleteComment(string app, string commentsId, string commentId)
{

5
backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkUpdateDto.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Infrastructure.Reflection;
@ -19,7 +18,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
/// The contents to update or insert.
/// </summary>
[LocalizedRequired]
public List<BulkUpdateJobDto> Jobs { get; set; }
public BulkUpdateJobDto[]? Jobs { get; set; }
/// <summary>
/// True to automatically publish the content.
@ -40,7 +39,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
{
var result = SimpleMapper.Map(this, new BulkUpdateContents());
result.Jobs = Jobs?.Select(x => x.ToJob())?.ToList();
result.Jobs = Jobs?.Select(x => x.ToJob())?.ToArray();
return result;
}

1
backend/src/Squidex/Areas/Api/Controllers/Contents/Models/GraphQLGetDto.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using GraphQL;
using GraphQL.NewtonsoftJson;
using Squidex.Domain.Apps.Entities.Contents.GraphQL;
using Squidex.Infrastructure.Reflection;

1
backend/src/Squidex/Areas/Api/Controllers/Contents/Models/GraphQLPostDto.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using GraphQL;
using GraphQL.NewtonsoftJson;
using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Entities.Contents.GraphQL;

2
backend/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs

@ -42,7 +42,7 @@ namespace Squidex.Areas.Api.Controllers.History
[HttpGet]
[Route("apps/{app}/history/")]
[ProducesResponseType(typeof(HistoryEventDto), 200)]
[ApiPermissionOrAnonymous(Permissions.AppCommon)]
[ApiPermissionOrAnonymous(Permissions.AppHistory)]
[ApiCosts(0.1)]
public async Task<IActionResult> GetHistory(string app, string channel)
{

2
backend/src/Squidex/Areas/Api/Controllers/Ping/PingController.cs

@ -68,7 +68,7 @@ namespace Squidex.Areas.Api.Controllers.Ping
/// </remarks>
[HttpGet]
[Route("ping/{app}/")]
[ApiPermissionOrAnonymous(Permissions.AppCommon)]
[ApiPermissionOrAnonymous(Permissions.AppPing)]
[ApiCosts(0)]
public IActionResult GetAppPing(string app)
{

3
backend/src/Squidex/Areas/Api/Controllers/QueryDto.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Entities;
@ -18,7 +17,7 @@ namespace Squidex.Areas.Api.Controllers
/// <summary>
/// The optional list of ids to query.
/// </summary>
public List<DomainId>? Ids { get; set; }
public DomainId[]? Ids { get; set; }
/// <summary>
/// The optional odata query.

5
backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/ConfigureFieldRulesDto.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using Squidex.Domain.Apps.Entities.Schemas.Commands;
@ -16,13 +15,13 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
/// <summary>
/// The field rules to configure.
/// </summary>
public List<FieldRuleDto>? FieldRules { get; set; }
public FieldRuleDto[]? FieldRules { get; set; }
public ConfigureFieldRules ToCommand()
{
return new ConfigureFieldRules
{
FieldRules = FieldRules?.Select(x => x.ToCommand()).ToList()
FieldRules = FieldRules?.Select(x => x.ToCommand()).ToArray()
};
}
}

3
backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/ReorderFieldsDto.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using Squidex.Domain.Apps.Entities.Schemas.Commands;
using Squidex.Infrastructure.Validation;
@ -17,7 +16,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
/// The field ids in the target order.
/// </summary>
[LocalizedRequired]
public List<long> FieldIds { get; set; }
public long[] FieldIds { get; set; }
public ReorderFields ToCommand(long? parentId = null)
{

22
backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaDto.cs

@ -27,17 +27,17 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
/// <summary>
/// The names of the fields that should be used in references.
/// </summary>
public List<string>? FieldsInReferences { get; set; }
public string[]? FieldsInReferences { get; set; }
/// <summary>
/// The names of the fields that should be shown in lists, including meta fields.
/// </summary>
public List<string>? FieldsInLists { get; set; }
public string[]? FieldsInLists { get; set; }
/// <summary>
/// Optional fields.
/// </summary>
public List<UpsertSchemaFieldDto?>? Fields { get; set; }
public UpsertSchemaFieldDto[]? Fields { get; set; }
/// <summary>
/// The optional preview urls.
@ -83,9 +83,9 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
command.FieldsInReferences = new FieldNames(dto.FieldsInReferences);
}
if (dto.Fields != null)
if (dto.Fields?.Length > 0)
{
command.Fields = new List<UpsertSchemaField>();
var fields = new List<UpsertSchemaField>();
foreach (var rootFieldDto in dto.Fields)
{
@ -96,9 +96,9 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
{
SimpleMapper.Map(rootFieldDto, rootField);
if (rootFieldDto?.Nested?.Count > 0)
if (rootFieldDto?.Nested?.Length > 0)
{
rootField.Nested = new List<UpsertSchemaNestedField>();
var nestedFields = new List<UpsertSchemaNestedField>();
foreach (var nestedFieldDto in rootFieldDto.Nested)
{
@ -110,13 +110,17 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
SimpleMapper.Map(nestedFieldDto, nestedField);
}
rootField.Nested.Add(nestedField);
nestedFields.Add(nestedField);
}
rootField.Nested = nestedFields.ToArray();
}
}
command.Fields.Add(rootField);
fields.Add(rootField);
}
command.Fields = fields.ToArray();
}
return command;

3
backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaFieldDto.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using Squidex.Infrastructure.Validation;
namespace Squidex.Areas.Api.Controllers.Schemas.Models
@ -48,6 +47,6 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
/// <summary>
/// The nested fields.
/// </summary>
public List<UpsertSchemaNestedFieldDto>? Nested { get; set; }
public UpsertSchemaNestedFieldDto[]? Nested { get; set; }
}
}

4
backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs

@ -44,7 +44,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
[HttpGet]
[Route("apps/{app}/schemas/")]
[ProducesResponseType(typeof(SchemasDto), 200)]
[ApiPermissionOrAnonymous(Permissions.AppCommon)]
[ApiPermissionOrAnonymous(Permissions.AppSchemasRead)]
[ApiCosts(0)]
public async Task<IActionResult> GetSchemas(string app)
{
@ -72,7 +72,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
[HttpGet]
[Route("apps/{app}/schemas/{name}/")]
[ProducesResponseType(typeof(SchemaDetailsDto), 200)]
[ApiPermissionOrAnonymous(Permissions.AppCommon)]
[ApiPermissionOrAnonymous(Permissions.AppSchemasRead)]
[ApiCosts(0)]
public async Task<IActionResult> GetSchema(string app, string name)
{

4
backend/src/Squidex/Areas/Api/Controllers/Search/SearchController.cs

@ -42,9 +42,9 @@ namespace Squidex.Areas.Api.Controllers.Search
[HttpGet]
[Route("apps/{app}/search/")]
[ProducesResponseType(typeof(SearchResultDto[]), 200)]
[ApiPermissionOrAnonymous(Permissions.AppCommon)]
[ApiPermissionOrAnonymous(Permissions.AppSearch)]
[ApiCosts(0)]
public async Task<IActionResult> GetSchemas(string app, [FromQuery] string? query = null)
public async Task<IActionResult> GetSearchResults(string app, [FromQuery] string? query = null)
{
var result = await searchManager.SearchAsync(query, Context);

8
backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs

@ -66,7 +66,7 @@ namespace Squidex.Areas.Api.Controllers.Statistics
[HttpGet]
[Route("apps/{app}/usages/log/")]
[ProducesResponseType(typeof(LogDownloadDto), 200)]
[ApiPermissionOrAnonymous(Permissions.AppCommon)]
[ApiPermissionOrAnonymous(Permissions.AppUsage)]
[ApiCosts(0)]
public IActionResult GetLog(string app)
{
@ -93,7 +93,7 @@ namespace Squidex.Areas.Api.Controllers.Statistics
[HttpGet]
[Route("apps/{app}/usages/calls/{fromDate}/{toDate}/")]
[ProducesResponseType(typeof(CallsUsageDtoDto), 200)]
[ApiPermissionOrAnonymous(Permissions.AppCommon)]
[ApiPermissionOrAnonymous(Permissions.AppUsage)]
[ApiCosts(0)]
public async Task<IActionResult> GetUsages(string app, DateTime fromDate, DateTime toDate)
{
@ -122,7 +122,7 @@ namespace Squidex.Areas.Api.Controllers.Statistics
[HttpGet]
[Route("apps/{app}/usages/storage/today/")]
[ProducesResponseType(typeof(CurrentStorageDto), 200)]
[ApiPermissionOrAnonymous(Permissions.AppCommon)]
[ApiPermissionOrAnonymous(Permissions.AppUsage)]
[ApiCosts(0)]
public async Task<IActionResult> GetCurrentStorageSize(string app)
{
@ -149,7 +149,7 @@ namespace Squidex.Areas.Api.Controllers.Statistics
[HttpGet]
[Route("apps/{app}/usages/storage/{fromDate}/{toDate}/")]
[ProducesResponseType(typeof(StorageUsagePerDateDto[]), 200)]
[ApiPermissionOrAnonymous(Permissions.AppCommon)]
[ApiPermissionOrAnonymous(Permissions.AppUsage)]
[ApiCosts(0)]
public async Task<IActionResult> GetStorageSizes(string app, DateTime fromDate, DateTime toDate)
{

4
backend/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs

@ -40,9 +40,9 @@ namespace Squidex.Areas.Api.Controllers.Translations
[HttpPost]
[Route("apps/{app}/translations/")]
[ProducesResponseType(typeof(TranslationDto), 200)]
[ApiPermissionOrAnonymous(Permissions.AppCommon)]
[ApiPermissionOrAnonymous(Permissions.AppTranslate)]
[ApiCosts(0)]
public async Task<IActionResult> GetLanguages(string app, [FromBody] TranslateDto request)
public async Task<IActionResult> PostTranslation(string app, [FromBody] TranslateDto request)
{
var result = await translator.Translate(request.Text, request.TargetLanguage, request.SourceLanguage, HttpContext.RequestAborted);
var response = TranslationDto.FromTranslation(result);

2
backend/src/Squidex/Areas/IdentityServer/Views/Account/Login.cshtml

@ -1,7 +1,7 @@
@model Squidex.Areas.IdentityServer.Controllers.Account.LoginVM
@{
var action = Model.IsLogin ? @T.Get("common.login") : @T.Get("common.signup");
var action = Model.IsLogin ? T.Get("common.login") : T.Get("common.signup");
ViewBag.Title = action;
}

2
backend/src/Squidex/Areas/IdentityServer/Views/Error/Error.cshtml

@ -3,7 +3,7 @@
@{
ViewBag.Theme = "white";
ViewBag.Title = @T.Get("users.error.title");
ViewBag.Title = T.Get("users.error.title");
}
<img class="splash-image" src="@Url.RootContentUrl("~/squid.svg?title=OH%20DAMN&text=I%20am%20sorry%2C%20that%20something%20went%20wrong")" />

2
backend/src/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml

@ -3,7 +3,7 @@
@{
ViewBag.Class = "profile-lg";
ViewBag.Title = @T.Get("users.profile.title");
ViewBag.Title = T.Get("users.profile.title");
void RenderValidation(string field)
{

1
backend/src/Squidex/Config/Domain/SerializationServices.cs

@ -49,6 +49,7 @@ namespace Squidex.Config.Domain
new NamedStringIdConverter(),
new PropertyPathConverter(),
new RefTokenConverter(),
new RoleConverter(),
new RolesConverter(),
new RuleConverter(),
new SchemaConverter(),

22
backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RoleTests.cs

@ -16,7 +16,7 @@ namespace Squidex.Domain.Apps.Core.Model.Apps
[Fact]
public void Should_be_default_role()
{
var role = new Role("Owner");
var role = Role.Create("Owner");
Assert.True(role.IsDefault);
}
@ -24,25 +24,25 @@ namespace Squidex.Domain.Apps.Core.Model.Apps
[Fact]
public void Should_not_be_default_role()
{
var role = new Role("Custom");
var role = Role.Create("Custom");
Assert.False(role.IsDefault);
}
[Fact]
public void Should_add_common_permission()
public void Should_not_add_common_permission()
{
var role = new Role("Name");
var role = Role.Create("Name");
var result = role.ForApp("my-app").Permissions.ToIds();
Assert.Equal(new[] { "squidex.apps.my-app.common" }, result);
Assert.Empty(result);
}
[Fact]
public void Should_not_have_duplicate_permission()
{
var role = new Role("Name", "common", "common", "common");
var role = Role.WithPermissions("Name", "common", "common", "common");
var result = role.ForApp("my-app").Permissions.ToIds();
@ -50,19 +50,19 @@ namespace Squidex.Domain.Apps.Core.Model.Apps
}
[Fact]
public void Should_ForApp_permission()
public void Should_append_app_prefix_to_permission()
{
var role = new Role("Name", "clients.read");
var role = Role.WithPermissions("Name", "clients.read");
var result = role.ForApp("my-app").Permissions.ToIds();
Assert.Equal("squidex.apps.my-app.clients.read", result.ElementAt(1));
Assert.Equal("squidex.apps.my-app.clients.read", result.ElementAt(0));
}
[Fact]
public void Should_check_for_name()
{
var role = new Role("Custom");
var role = Role.WithPermissions("Custom");
Assert.True(role.Equals("Custom"));
}
@ -70,7 +70,7 @@ namespace Squidex.Domain.Apps.Core.Model.Apps
[Fact]
public void Should_check_for_null_name()
{
var role = new Role("Custom");
var role = Role.WithPermissions("Custom");
Assert.False(role.Equals((string)null!));
Assert.False(role.Equals("Other"));

39
backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesJsonTests.cs

@ -5,19 +5,56 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using FluentAssertions;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Security;
using Xunit;
namespace Squidex.Domain.Apps.Core.Model.Apps
{
public class RolesJsonTests
{
[Fact]
public void Should_deserialize_from_old_role_format()
{
var source = new Dictionary<string, string[]>
{
["Custom"] = new[]
{
"Permission1",
"Permission2"
}
};
var expected =
Roles.Empty
.Add("Custom")
.Update("Custom",
new PermissionSet(
"Permission1",
"Permission2"));
var roles = source.SerializeAndDeserialize<Roles>();
roles.Should().BeEquivalentTo(expected);
}
[Fact]
public void Should_serialize_and_deserialize()
{
var sut = Roles.Empty.Add("Custom").Update("Custom", "Permission1", "Permission2");
var sut =
Roles.Empty
.Add("Custom")
.Update("Custom",
new PermissionSet(
"Permission1",
"Permission2"),
JsonValue.Object()
.Add("Property1", true)
.Add("Property2", true));
var roles = sut.SerializeAndDeserialize();

51
backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs

@ -9,6 +9,7 @@ using System.Collections.Generic;
using System.Linq;
using FluentAssertions;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Security;
using Xunit;
@ -40,7 +41,7 @@ namespace Squidex.Domain.Apps.Core.Model.Apps
{
var roles_1 = roles_0.Add(role);
roles_1[role].Should().BeEquivalentTo(new Role(role, PermissionSet.Empty));
roles_1[role].Should().BeEquivalentTo(Role.Create(role));
}
[Fact]
@ -61,15 +62,23 @@ namespace Squidex.Domain.Apps.Core.Model.Apps
}
[Fact]
public void Should_update_role()
public void Should_update_role_permissions()
{
var roles_1 = roles_0.Update(firstRole, "P1", "P2");
var roles_1 = roles_0.Update(firstRole, permissions: new PermissionSet("P1", "P2"));
roles_1[firstRole].Should().BeEquivalentTo(new Role(firstRole, new PermissionSet("P1", "P2")));
roles_1[firstRole].Should().BeEquivalentTo(Role.WithPermissions(firstRole, "P1", "P2"));
}
[Fact]
public void Should_return_same_roles_if_role_is_updated_with_same_values()
public void Should_update_role_properties()
{
var roles_1 = roles_0.Update(firstRole, properties: JsonValue.Object().Add("P1", true));
roles_1[firstRole].Should().BeEquivalentTo(Role.WithProperties(firstRole, JsonValue.Object().Add("P1", true)));
}
[Fact]
public void Should_return_same_roles_if_role_is_updated_with_same_permissions()
{
var roles_1 = roles_0.Update(firstRole);
@ -79,7 +88,7 @@ namespace Squidex.Domain.Apps.Core.Model.Apps
[Fact]
public void Should_return_same_roles_if_role_not_found()
{
var roles_1 = roles_0.Update(role, "P1", "P2");
var roles_1 = roles_0.Update(role, permissions: new PermissionSet("P1", "P2"));
Assert.Same(roles_0, roles_1);
}
@ -142,14 +151,14 @@ namespace Squidex.Domain.Apps.Core.Model.Apps
Assert.False(Roles.IsDefault(firstRole));
}
[InlineData("Developer")]
[InlineData("Editor")]
[InlineData("Owner")]
[InlineData("Reader")]
[InlineData("Developer", 7)]
[InlineData("Editor", 4)]
[InlineData("Reader", 2)]
[InlineData("Owner", 1)]
[Theory]
public void Should_get_default_roles(string name)
public void Should_get_default_roles(string name, int permissionCount)
{
var found = roles_0.TryGet("app", name, out var result);
var found = roles_0.TryGet("app", name, false, out var result);
Assert.True(found);
Assert.True(result!.IsDefault);
@ -158,13 +167,29 @@ namespace Squidex.Domain.Apps.Core.Model.Apps
foreach (var permission in result.Permissions)
{
Assert.StartsWith("squidex.apps.app.", permission.Id);
Assert.DoesNotContain("{app}", permission.Id);
}
Assert.Equal(permissionCount, result!.Permissions.Count);
}
[InlineData("Developer", 17)]
[InlineData("Editor", 14)]
[InlineData("Reader", 13)]
[InlineData("Owner", 1)]
[Theory]
public void Should_add_extra_permissions_for_frontend_client(string name, int permissionCount)
{
var found = roles_0.TryGet("app", name, true, out var result);
Assert.True(found);
Assert.Equal(permissionCount, result!.Permissions.Count);
}
[Fact]
public void Should_return_null_if_role_not_found()
{
var found = roles_0.TryGet("app", "custom", out var result);
var found = roles_0.TryGet("app", "custom", false, out var result);
Assert.False(found);
Assert.Null(result);

10
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/SchemaSynchronizerTests.cs

@ -575,7 +575,7 @@ namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization
var events = sourceSchema.Synchronize(targetSchema, idGenerator);
events.ShouldHaveSameEvents(
new SchemaFieldsReordered { FieldIds = new List<long> { 11, 10 }, ParentFieldId = arrayId }
new SchemaFieldsReordered { FieldIds = new[] { 11L, 10L }, ParentFieldId = arrayId }
);
}
@ -595,7 +595,7 @@ namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization
var events = sourceSchema.Synchronize(targetSchema, idGenerator);
events.ShouldHaveSameEvents(
new SchemaFieldsReordered { FieldIds = new List<long> { 11, 10 } }
new SchemaFieldsReordered { FieldIds = new[] { 11L, 10L } }
);
}
@ -617,7 +617,7 @@ namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization
events.ShouldHaveSameEvents(
new FieldDeleted { FieldId = NamedId.Of(11L, "f2") },
new FieldAdded { FieldId = NamedId.Of(50L, "f3"), Name = "f3", Partitioning = Partitioning.Invariant.Key, Properties = new StringFieldProperties() },
new SchemaFieldsReordered { FieldIds = new List<long> { 50, 10 } }
new SchemaFieldsReordered { FieldIds = new[] { 50L, 10L } }
);
}
@ -639,7 +639,7 @@ namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization
events.ShouldHaveSameEvents(
new FieldAdded { FieldId = NamedId.Of(50L, "f3"), Name = "f3", Partitioning = Partitioning.Invariant.Key, Properties = new StringFieldProperties() },
new SchemaFieldsReordered { FieldIds = new List<long> { 10, 50, 11 } }
new SchemaFieldsReordered { FieldIds = new[] { 10L, 50L, 11L } }
);
}
@ -661,7 +661,7 @@ namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization
events.ShouldHaveSameEvents(
new FieldDeleted { FieldId = NamedId.Of(10L, "f1") },
new FieldAdded { FieldId = NamedId.Of(50L, "f3"), Name = "f3", Partitioning = Partitioning.Invariant.Key, Properties = new StringFieldProperties() },
new SchemaFieldsReordered { FieldIds = new List<long> { 50, 11 } }
new SchemaFieldsReordered { FieldIds = new[] { 50L, 11L } }
);
}
}

4
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/ExpressionsAttribute.cs

@ -44,7 +44,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
{
yield return new object[]
{
string.Format("Script(`{0}`)", script)
$"Script(`{script}`)"
};
}
@ -52,7 +52,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
{
yield return new object[]
{
string.Format("Liquid({0})", liquid)
$"Liquid({liquid})"
};
}
}

2
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterCompareTests.cs

@ -46,7 +46,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
{
public (bool Match, ValueTask<string?>) Format(EnrichedEvent @event, object value, string[] path)
{
if (path[0] == "data" && value is JsonArray _)
if (path[0] == "data" && value is JsonArray)
{
return (true, GetValueAsync());
}

2
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs

@ -42,7 +42,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
{
public (bool Match, ValueTask<string?>) Format(EnrichedEvent @event, object value, string[] path)
{
if (path[0] == "data" && value is JsonArray _)
if (path[0] == "data" && value is JsonArray)
{
return (true, GetValueAsync());
}

3
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineHelperTests.cs

@ -42,7 +42,8 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
sut = new JintScriptEngine(cache, extensions)
{
Timeout = TimeSpan.FromSeconds(1)
TimeoutScript = TimeSpan.FromSeconds(10),
TimeoutExecution = TimeSpan.FromSeconds(2)
};
}

3
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs

@ -58,7 +58,8 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
sut = new JintScriptEngine(cache, extensions)
{
Timeout = TimeSpan.FromSeconds(1)
TimeoutScript = TimeSpan.FromSeconds(10),
TimeoutExecution = TimeSpan.FromSeconds(2)
};
}

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

Loading…
Cancel
Save