Browse Source

Role properties. (#580)

* Role properties.

* Imrproved permission system.

* Tests fixed.

* Design improvements.

* Tests fixed.

* Make properties optional.
pull/581/head
Sebastian Stehle 5 years ago
committed by GitHub
parent
commit
eaa33d0354
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      backend/extensions/Squidex.Extensions/Actions/RuleHelper.cs
  2. 10
      backend/i18n/frontend_en.json
  3. 10
      backend/i18n/frontend_it.json
  4. 10
      backend/i18n/frontend_nl.json
  5. 10
      backend/i18n/source/frontend_en.json
  6. 2
      backend/src/Migrations/OldEvents/SchemaCreated.cs
  7. 11
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs
  8. 13
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs
  9. 4
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs
  10. 21
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonRole.cs
  11. 72
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/RoleConverter.cs
  12. 22
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/RolesConverter.cs
  13. 65
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs
  14. 76
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs
  15. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs
  16. 11
      backend/src/Squidex.Domain.Apps.Entities/Apps/AppSettingsSearchSource.cs
  17. 3
      backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateLanguage.cs
  18. 4
      backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateRole.cs
  19. 2
      backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppLanguages.cs
  20. 6
      backend/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs
  21. 12
      backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/SchemaBuilder.cs
  22. 6
      backend/src/Squidex.Domain.Apps.Entities/Contents/BulkUpdateCommandMiddleware.cs
  23. 3
      backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/BulkUpdateContents.cs
  24. 5
      backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureFieldRules.cs
  25. 4
      backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ReorderFields.cs
  26. 9
      backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertCommand.cs
  27. 4
      backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaField.cs
  28. 12
      backend/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs
  29. 3
      backend/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs
  30. 3
      backend/src/Squidex.Domain.Apps.Events/Apps/AppLanguageUpdated.cs
  31. 6
      backend/src/Squidex.Domain.Apps.Events/Apps/AppPlanChanged.cs
  32. 9
      backend/src/Squidex.Domain.Apps.Events/Apps/AppRoleUpdated.cs
  33. 4
      backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedField.cs
  34. 3
      backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaFieldsReordered.cs
  35. 12
      backend/src/Squidex.Infrastructure/EventSourcing/Grains/BatchSubscriber.cs
  36. 22
      backend/src/Squidex.Shared/Permissions.cs
  37. 23
      backend/src/Squidex.Web/Pipeline/AppResolver.cs
  38. 2
      backend/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs
  39. 2
      backend/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs
  40. 2
      backend/src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs
  41. 12
      backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs
  42. 36
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs
  43. 15
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/RoleDto.cs
  44. 3
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateLanguageDto.cs
  45. 8
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateRoleDto.cs
  46. 2
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateWorkflowDto.cs
  47. 2
      backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetContentQueryDto.cs
  48. 8
      backend/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs
  49. 5
      backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkUpdateDto.cs
  50. 2
      backend/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs
  51. 2
      backend/src/Squidex/Areas/Api/Controllers/Ping/PingController.cs
  52. 3
      backend/src/Squidex/Areas/Api/Controllers/QueryDto.cs
  53. 5
      backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/ConfigureFieldRulesDto.cs
  54. 3
      backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/ReorderFieldsDto.cs
  55. 22
      backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaDto.cs
  56. 3
      backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaFieldDto.cs
  57. 4
      backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs
  58. 4
      backend/src/Squidex/Areas/Api/Controllers/Search/SearchController.cs
  59. 8
      backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs
  60. 4
      backend/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs
  61. 1
      backend/src/Squidex/Config/Domain/SerializationServices.cs
  62. 22
      backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RoleTests.cs
  63. 39
      backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesJsonTests.cs
  64. 51
      backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs
  65. 10
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/SchemaSynchronizerTests.cs
  66. 1
      backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TestUtils.cs
  67. 10
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs
  68. 79
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppSettingsSearchSourceTests.cs
  69. 7
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppLanguagesTests.cs
  70. 17
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppRolesTests.cs
  71. 21
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/BulkUpdateCommandMiddlewareTests.cs
  72. 52
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaTests.cs
  73. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaCommandsTests.cs
  74. 10
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaDomainObjectTests.cs
  75. 81
      backend/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs
  76. 5
      frontend/app/features/apps/pages/apps-page.component.ts
  77. 5
      frontend/app/features/assets/pages/assets-page.component.ts
  78. 4
      frontend/app/features/content/pages/content/content-field.component.ts
  79. 4
      frontend/app/features/content/pages/content/content-section.component.ts
  80. 6
      frontend/app/features/content/pages/schemas/schemas-page.component.ts
  81. 4
      frontend/app/features/content/shared/forms/assets-editor.component.ts
  82. 4
      frontend/app/features/content/shared/preview-button.component.ts
  83. 6
      frontend/app/features/dashboard/pages/dashboard-page.component.ts
  84. 2
      frontend/app/features/settings/pages/languages/language.component.ts
  85. 5
      frontend/app/features/settings/pages/languages/languages-page.component.html
  86. 84
      frontend/app/features/settings/pages/roles/role.component.html
  87. 24
      frontend/app/features/settings/pages/roles/role.component.scss
  88. 55
      frontend/app/features/settings/pages/roles/role.component.ts
  89. 4
      frontend/app/features/settings/pages/roles/roles-page.component.html
  90. 7
      frontend/app/features/settings/pages/roles/roles-page.component.ts
  91. 3
      frontend/app/framework/angular/forms/editors/autocomplete.component.html
  92. 6
      frontend/app/framework/angular/forms/editors/autocomplete.component.ts
  93. 2
      frontend/app/framework/angular/forms/editors/checkbox-group.component.scss
  94. 6
      frontend/app/shared/components/assets/assets-selector.component.ts
  95. 6
      frontend/app/shared/components/forms/geolocation-editor.component.ts
  96. 6
      frontend/app/shared/components/schema-category.component.ts
  97. 1
      frontend/app/shared/internal.ts
  98. 10
      frontend/app/shared/services/apps.service.spec.ts
  99. 2
      frontend/app/shared/services/apps.service.ts
  100. 26
      frontend/app/shared/services/roles.service.spec.ts

2
backend/extensions/Squidex.Extensions/Actions/RuleHelper.cs

@ -18,7 +18,7 @@ namespace Squidex.Extensions.Actions
{ {
public static class RuleHelper public static class RuleHelper
{ {
public static bool ShouldDelete(this EnrichedEvent @event, IScriptEngine scriptEngine, string? expression) public static bool ShouldDelete(this EnrichedEvent @event, IScriptEngine scriptEngine, string expression)
{ {
if (!string.IsNullOrWhiteSpace(expression)) if (!string.IsNullOrWhiteSpace(expression))
{ {

10
backend/i18n/frontend_en.json

@ -550,6 +550,16 @@
"roles.deleteConfirmTitle": "Do you really want to delete the role?", "roles.deleteConfirmTitle": "Do you really want to delete the role?",
"roles.loadFailed": "Failed to load roles. Please reload.", "roles.loadFailed": "Failed to load roles. Please reload.",
"roles.loadPermissionsFailed": "Failed to load permissions. 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.refreshTooltip": "Refresh roles (CTRL + SHIFT + R)",
"roles.reloaded": "Roles reloaded.", "roles.reloaded": "Roles reloaded.",
"roles.revokeFailed": "Failed to revoke role. Please reload.", "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.deleteConfirmTitle": "Sei sicuro di voler eliminare il ruolo?",
"roles.loadFailed": "Non è stato possibile caricare i ruoli. Per favore ricarica.", "roles.loadFailed": "Non è stato possibile caricare i ruoli. Per favore ricarica.",
"roles.loadPermissionsFailed": "Non è stato possibile caricare i permessi. 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.refreshTooltip": "Aggiorna i ruoli (CTRL + SHIFT + R)",
"roles.reloaded": "Ruoli ricaricati.", "roles.reloaded": "Ruoli ricaricati.",
"roles.revokeFailed": "Non è stato possibile rimuovere il ruolo. Per favore ricarica.", "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.deleteConfirmTitle": "Wil je de rol echt verwijderen?",
"roles.loadFailed": "Laden van rollen is mislukt. Laad opnieuw.", "roles.loadFailed": "Laden van rollen is mislukt. Laad opnieuw.",
"roles.loadPermissionsFailed": "Kan machtigingen niet laden. 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.refreshTooltip": "Ververs rollen (CTRL + SHIFT + R)",
"roles.reloaded": "Rollen opnieuw geladen.", "roles.reloaded": "Rollen opnieuw geladen.",
"roles.revokeFailed": "Kan rol niet intrekken. Laad opnieuw.", "roles.revokeFailed": "Kan rol niet intrekken. Laad opnieuw.",

10
backend/i18n/source/frontend_en.json

@ -550,6 +550,16 @@
"roles.deleteConfirmTitle": "Do you really want to delete the role?", "roles.deleteConfirmTitle": "Do you really want to delete the role?",
"roles.loadFailed": "Failed to load roles. Please reload.", "roles.loadFailed": "Failed to load roles. Please reload.",
"roles.loadPermissionsFailed": "Failed to load permissions. 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.refreshTooltip": "Refresh roles (CTRL + SHIFT + R)",
"roles.reloaded": "Roles reloaded.", "roles.reloaded": "Roles reloaded.",
"roles.revokeFailed": "Failed to revoke role. Please reload.", "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); 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) 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; Role = role;
ApiCallsLimit = apiCallsLimit; ApiCallsLimit = apiCallsLimit;
ApiTrafficLimit = apiTrafficLimit; ApiTrafficLimit = apiTrafficLimit;
AllowAnonymous = allowAnonymous; AllowAnonymous = allowAnonymous;
} }
[Pure] [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)); 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] [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)); Guard.NotNullOrEmpty(id, nameof(id));
@ -66,7 +71,9 @@ namespace Squidex.Domain.Apps.Core.Apps
return this; 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 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) 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) 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); 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) 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) if (json.Count == 0)
{ {
return Roles.Empty; 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.Contracts; using System.Diagnostics.Contracts;
using System.Linq; using System.Linq;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Security;
using P = Squidex.Shared.Permissions; using P = Squidex.Shared.Permissions;
@ -18,36 +20,68 @@ namespace Squidex.Domain.Apps.Core.Apps
[Equals(DoNotAddEqualityOperators = true)] [Equals(DoNotAddEqualityOperators = true)]
public sealed class Role : Named 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 Editor = "Editor";
public const string Developer = "Developer"; public const string Developer = "Developer";
public const string Owner = "Owner"; public const string Owner = "Owner";
public const string Reader = "Reader"; public const string Reader = "Reader";
public static readonly ReadOnlyCollection<string> EmptyProperties = new ReadOnlyCollection<string>(new List<string>());
public PermissionSet Permissions { get; } public PermissionSet Permissions { get; }
public JsonObject Properties { get; }
[IgnoreDuringEquals] [IgnoreDuringEquals]
public bool IsDefault public bool IsDefault
{ {
get { return Roles.IsDefault(this); } get { return Roles.IsDefault(this); }
} }
public Role(string name, PermissionSet permissions) public Role(string name, PermissionSet permissions, JsonObject properties)
: base(name) : base(name)
{ {
Guard.NotNull(permissions, nameof(permissions)); Guard.NotNull(permissions, nameof(permissions));
Guard.NotNull(properties, nameof(properties));
Permissions = permissions; 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) public static Role Create(string role)
: this(name, new PermissionSet(permissions))
{ {
return new Role(role, PermissionSet.Empty, JsonValue.Object());
} }
[Pure] [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) public bool Equals(string name)
@ -55,12 +89,11 @@ namespace Squidex.Domain.Apps.Core.Apps
return name != null && name.Equals(Name, StringComparison.Ordinal); 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> Guard.NotNullOrEmpty(app, nameof(app));
{
P.ForApp(P.AppCommon, app) var result = new HashSet<Permission>();
};
if (Permissions.Any()) 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 System.Linq;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.Collections;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Security;
using Squidex.Shared; using Squidex.Shared;
@ -24,28 +25,37 @@ namespace Squidex.Domain.Apps.Core.Apps
public static readonly IReadOnlyDictionary<string, Role> Defaults = new Dictionary<string, Role> public static readonly IReadOnlyDictionary<string, Role> Defaults = new Dictionary<string, Role>
{ {
[Role.Owner] = [Role.Owner] =
new Role(Role.Owner, new PermissionSet( new Role(Role.Owner,
Clean(Permissions.App))), new PermissionSet(
Clean(Permissions.App)),
JsonValue.Object()),
[Role.Reader] = [Role.Reader] =
new Role(Role.Reader, new PermissionSet( new Role(Role.Reader,
Clean(Permissions.AppAssetsRead), new PermissionSet(
Clean(Permissions.AppContentsRead))), Clean(Permissions.AppAssetsRead),
Clean(Permissions.AppContentsRead)),
JsonValue.Object()
.Add("ui.api.hide", true)),
[Role.Editor] = [Role.Editor] =
new Role(Role.Editor, new PermissionSet( new Role(Role.Editor,
Clean(Permissions.AppAssets), new PermissionSet(
Clean(Permissions.AppContents), Clean(Permissions.AppAssets),
Clean(Permissions.AppRolesRead), Clean(Permissions.AppContents),
Clean(Permissions.AppWorkflowsRead))), Clean(Permissions.AppRolesRead),
Clean(Permissions.AppWorkflowsRead)),
JsonValue.Object()
.Add("ui.api.hide", true)),
[Role.Developer] = [Role.Developer] =
new Role(Role.Developer, new PermissionSet( new Role(Role.Developer,
Clean(Permissions.AppApi), new PermissionSet(
Clean(Permissions.AppAssets), Clean(Permissions.AppAssets),
Clean(Permissions.AppContents), Clean(Permissions.AppContents),
Clean(Permissions.AppPatterns), Clean(Permissions.AppPatterns),
Clean(Permissions.AppRolesRead), Clean(Permissions.AppRolesRead),
Clean(Permissions.AppRules), Clean(Permissions.AppRules),
Clean(Permissions.AppSchemas), Clean(Permissions.AppSchemas),
Clean(Permissions.AppWorkflows))) Clean(Permissions.AppWorkflows)),
JsonValue.Object()),
}; };
public static readonly Roles Empty = new Roles(new ImmutableDictionary<string, Role>()); public static readonly Roles Empty = new Roles(new ImmutableDictionary<string, Role>());
@ -89,8 +99,6 @@ namespace Squidex.Domain.Apps.Core.Apps
[Pure] [Pure]
public Roles Add(string name) public Roles Add(string name)
{ {
var newRole = new Role(name);
if (inner.ContainsKey(name)) if (inner.ContainsKey(name))
{ {
return this; return this;
@ -101,21 +109,24 @@ namespace Squidex.Domain.Apps.Core.Apps
return this; return this;
} }
var newRole = Role.Create(name);
return Create(inner.With(name, newRole)); return Create(inner.With(name, newRole));
} }
[Pure] [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.NotNullOrEmpty(name, nameof(name));
Guard.NotNull(permissions, nameof(permissions));
if (!inner.TryGetValue(name, out var role)) if (!inner.TryGetValue(name, out var role))
{ {
return this; 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) public static bool IsDefault(string role)
@ -138,19 +149,22 @@ namespace Squidex.Domain.Apps.Core.Apps
return inner.ContainsKey(name) || Defaults.ContainsKey(name); 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)); 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); value = role.ForApp(app, isFrontend && name != Role.Owner);
return true; }
else if (inner.TryGetValue(name, out role))
{
value = role.ForApp(app, isFrontend);
} }
value = null!; return value != null;
return false;
} }
private static string Clean(string permission) 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)) 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 }; yield return new SchemaFieldsReordered { FieldIds = fieldIds, ParentFieldId = parentId };
} }

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

@ -56,19 +56,16 @@ namespace Squidex.Domain.Apps.Entities.Apps
Search("Clients", Permissions.AppClientsRead, Search("Clients", Permissions.AppClientsRead,
urlGenerator.ClientsUI, SearchResultType.Setting); urlGenerator.ClientsUI, SearchResultType.Setting);
Search("Contents", Permissions.AppCommon,
urlGenerator.ContentsUI, SearchResultType.Content);
Search("Contributors", Permissions.AppContributorsRead, Search("Contributors", Permissions.AppContributorsRead,
urlGenerator.ContributorsUI, SearchResultType.Setting); urlGenerator.ContributorsUI, SearchResultType.Setting);
Search("Dashboard", Permissions.AppCommon, Search("Dashboard", Permissions.AppUsage,
urlGenerator.DashboardUI, SearchResultType.Dashboard); urlGenerator.DashboardUI, SearchResultType.Dashboard);
Search("Languages", Permissions.AppCommon, Search("Languages", Permissions.AppLanguagesRead,
urlGenerator.LanguagesUI, SearchResultType.Setting); urlGenerator.LanguagesUI, SearchResultType.Setting);
Search("Patterns", Permissions.AppCommon, Search("Patterns", Permissions.AppPatternsRead,
urlGenerator.PatternsUI, SearchResultType.Setting); urlGenerator.PatternsUI, SearchResultType.Setting);
Search("Roles", Permissions.AppRolesRead, Search("Roles", Permissions.AppRolesRead,
@ -77,7 +74,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
Search("Rules", Permissions.AppRulesRead, Search("Rules", Permissions.AppRulesRead,
urlGenerator.RulesUI, SearchResultType.Rule); urlGenerator.RulesUI, SearchResultType.Rule);
Search("Schemas", Permissions.AppCommon, Search("Schemas", Permissions.AppSchemasRead,
urlGenerator.SchemasUI, SearchResultType.Schema); urlGenerator.SchemasUI, SearchResultType.Schema);
Search("Subscription", Permissions.AppPlansRead, 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. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Collections.Generic;
using Squidex.Infrastructure; using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Apps.Commands namespace Squidex.Domain.Apps.Entities.Apps.Commands
@ -18,6 +17,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.Commands
public bool IsMaster { get; set; } 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. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Entities.Apps.Commands namespace Squidex.Domain.Apps.Entities.Apps.Commands
{ {
public sealed class UpdateRole : AppCommand public sealed class UpdateRole : AppCommand
@ -12,5 +14,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Commands
public string Name { get; set; } public string Name { get; set; }
public string[] Permissions { 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)); 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)); e(T.Get("apps.languages.masterLanguageNoFallbacks"), nameof(command.Fallback));
} }

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

@ -70,7 +70,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.State
return UpdateImage(e, ev => null); return UpdateImage(e, ev => null);
case AppPlanChanged e when Is.Change(Plan?.PlanId, e.PlanId): 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: case AppPlanReset e when Plan != null:
return UpdatePlan(e, ev => null); return UpdatePlan(e, ev => null);
@ -112,7 +112,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.State
return UpdateRoles(e, (ev, r) => r.Add(ev.Name)); return UpdateRoles(e, (ev, r) => r.Add(ev.Name));
case AppRoleUpdated e: 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: case AppRoleDeleted e:
return UpdateRoles(e, (ev, r) => r.Remove(ev.Name)); return UpdateRoles(e, (ev, r) => r.Remove(ev.Name));
@ -126,7 +126,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.State
case AppLanguageUpdated e: case AppLanguageUpdated e:
return UpdateLanguages(e, (ev, l) => 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) if (ev.IsMaster)
{ {

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

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

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

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

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

@ -6,7 +6,6 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Collections.Generic;
using Squidex.Infrastructure; using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents.Commands namespace Squidex.Domain.Apps.Entities.Contents.Commands
@ -25,6 +24,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Commands
public bool OptimizeValidation { get; set; } public bool OptimizeValidation { get; set; }
public List<BulkUpdateJob>? Jobs { get; set; } public BulkUpdateJob[]? Jobs { get; set; }
} }
} }

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

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

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

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

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

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

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

@ -5,14 +5,12 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Collections.Generic;
namespace Squidex.Domain.Apps.Entities.Schemas.Commands namespace Squidex.Domain.Apps.Entities.Schemas.Commands
{ {
public sealed class UpsertSchemaField : UpsertSchemaFieldBase public sealed class UpsertSchemaField : UpsertSchemaFieldBase
{ {
public string Partitioning { get; set; } 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(UpsertCommand command, AddValidation e) private static void ValidateUpsert(UpsertCommand command, AddValidation e)
{ {
if (command.Fields?.Count > 0) if (command.Fields?.Length > 0)
{ {
command.Fields.Foreach((field, fieldIndex) => command.Fields.Foreach((field, fieldIndex) =>
{ {
@ -182,7 +182,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
ValidateField(field, prefix, e); ValidateField(field, prefix, e);
if (field.Nested?.Count > 0) if (field.Nested?.Length > 0)
{ {
if (field.Properties is ArrayFieldProperties) if (field.Properties is ArrayFieldProperties)
{ {
@ -193,7 +193,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
ValidateNestedField(nestedField, nestedPrefix, e); 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)}"); 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) => fieldRules?.Foreach((rule, ruleIndex) =>
{ {
@ -318,7 +318,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
{ {
var fieldPrefix = $"{path}[{fieldIndex}]"; 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)) 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) 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)); e(T.Get("schemas.fieldsNotCovered"), nameof(c.FieldIds));
} }

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

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

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

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Collections.Generic;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
@ -20,6 +19,6 @@ namespace Squidex.Domain.Apps.Events.Apps
public bool IsMaster { get; set; } 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. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Events.Apps namespace Squidex.Domain.Apps.Events.Apps
@ -13,5 +14,10 @@ namespace Squidex.Domain.Apps.Events.Apps
public sealed class AppPlanChanged : AppEvent public sealed class AppPlanChanged : AppEvent
{ {
public string PlanId { get; set; } 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.EventSourcing;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Security;
namespace Squidex.Domain.Apps.Events.Apps namespace Squidex.Domain.Apps.Events.Apps
{ {
@ -15,5 +17,12 @@ namespace Squidex.Domain.Apps.Events.Apps
public string Name { get; set; } public string Name { get; set; }
public string[] Permissions { 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. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Collections.Generic;
namespace Squidex.Domain.Apps.Events.Schemas namespace Squidex.Domain.Apps.Events.Schemas
{ {
public sealed class SchemaCreatedField : SchemaCreatedFieldBase public sealed class SchemaCreatedField : SchemaCreatedFieldBase
{ {
public string Partitioning { get; set; } 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. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Collections.Generic;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Events.Schemas namespace Squidex.Domain.Apps.Events.Schemas
@ -13,6 +12,6 @@ namespace Squidex.Domain.Apps.Events.Schemas
[EventType(nameof(SchemaFieldsReordered))] [EventType(nameof(SchemaFieldsReordered))]
public sealed class SchemaFieldsReordered : ParentFieldEvent public sealed class SchemaFieldsReordered : ParentFieldEvent
{ {
public List<long> FieldIds { get; set; } public long[] FieldIds { get; set; }
} }
} }

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

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

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 AdminUsersLock = "squidex.admin.users.lock";
public const string App = "squidex.apps.{app}"; 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 AppDelete = "squidex.apps.{app}.delete";
public const string AppUpdate = "squidex.apps.{app}.update"; public const string AppUpdate = "squidex.apps.{app}.update";
public const string AppUpdateImage = "squidex.apps.{app}.update"; public const string AppUpdateImage = "squidex.apps.{app}.update";
public const string AppUpdateGeneral = "squidex.apps.{app}.general"; 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 AppClients = "squidex.apps.{app}.clients";
public const string AppClientsRead = "squidex.apps.{app}.clients.read"; public const string AppClientsRead = "squidex.apps.{app}.clients.read";
public const string AppClientsCreate = "squidex.apps.{app}.clients.create"; 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 AppContributorsRevoke = "squidex.apps.{app}.contributors.revoke";
public const string AppLanguages = "squidex.apps.{app}.languages"; 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 AppLanguagesCreate = "squidex.apps.{app}.languages.create";
public const string AppLanguagesUpdate = "squidex.apps.{app}.languages.update"; public const string AppLanguagesUpdate = "squidex.apps.{app}.languages.update";
public const string AppLanguagesDelete = "squidex.apps.{app}.languages.delete"; 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 AppRolesDelete = "squidex.apps.{app}.roles.delete";
public const string AppPatterns = "squidex.apps.{app}.patterns"; 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 AppPatternsCreate = "squidex.apps.{app}.patterns.create";
public const string AppPatternsUpdate = "squidex.apps.{app}.patterns.update"; public const string AppPatternsUpdate = "squidex.apps.{app}.patterns.update";
public const string AppPatternsDelete = "squidex.apps.{app}.patterns.delete"; 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 AppRulesDelete = "squidex.apps.{app}.rules.delete";
public const string AppSchemas = "squidex.apps.{app}.schemas"; 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 AppSchemasCreate = "squidex.apps.{app}.schemas.create";
public const string AppSchemasUpdate = "squidex.apps.{app}.schemas.{name}.update"; public const string AppSchemasUpdate = "squidex.apps.{app}.schemas.{name}.update";
public const string AppSchemasScripts = "squidex.apps.{app}.schemas.{name}.scripts"; 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 AppContentsVersionDelete = "squidex.apps.{app}.contents.{name}.version.delete";
public const string AppContentsDelete = "squidex.apps.{app}.contents.{name}.delete"; public const string AppContentsDelete = "squidex.apps.{app}.contents.{name}.delete";
public const string AppApi = "squidex.apps.{app}.api";
static Permissions() static Permissions()
{ {
foreach (var field in typeof(Permissions).GetFields(BindingFlags.Public | BindingFlags.Static)) foreach (var field in typeof(Permissions).GetFields(BindingFlags.Public | BindingFlags.Static))

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

@ -6,6 +6,7 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Security.Claims; using System.Security.Claims;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -41,9 +42,9 @@ namespace Squidex.Web.Pipeline
if (!string.IsNullOrWhiteSpace(appName)) 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) if (app == null)
{ {
@ -51,16 +52,16 @@ namespace Squidex.Web.Pipeline
return; return;
} }
var (role, permissions) = FindByOpenIdSubject(app, user); var (role, permissions) = FindByOpenIdSubject(app, user, isFrontend);
if (permissions == null) if (permissions == null)
{ {
(role, permissions) = FindByOpenIdClient(app, user); (role, permissions) = FindByOpenIdClient(app, user, isFrontend);
} }
if (permissions == null) if (permissions == null)
{ {
(role, permissions) = FindAnonymousClient(app); (role, permissions) = FindAnonymousClient(app, isFrontend);
} }
if (permissions != null) if (permissions != null)
@ -132,7 +133,7 @@ namespace Squidex.Web.Pipeline
return context.ActionDescriptor.EndpointMetadata.Any(x => x is AllowAnonymousAttribute); 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(); var (appName, clientId) = user.GetClient();
@ -141,7 +142,7 @@ namespace Squidex.Web.Pipeline
return (null, null); 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); return (client.Role, role.Permissions);
} }
@ -149,11 +150,11 @@ namespace Squidex.Web.Pipeline
return (null, null); 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); 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); return (client.Role, role.Permissions);
} }
@ -161,11 +162,11 @@ namespace Squidex.Web.Pipeline
return (null, null); 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(); 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); return (roleName, role.Permissions);
} }

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

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

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

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

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

@ -80,7 +80,9 @@ namespace Squidex.Areas.Api.Controllers.Apps
var response = Deferred.Response(() => 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(); Response.Headers[HeaderNames.ETag] = apps.ToEtag();
@ -108,7 +110,9 @@ namespace Squidex.Areas.Api.Controllers.Apps
var userOrClientId = HttpContext.User.UserOrClientId()!; var userOrClientId = HttpContext.User.UserOrClientId()!;
var userPermissions = Resources.Permissions; var userPermissions = Resources.Permissions;
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(); Response.Headers[HeaderNames.ETag] = App.ToEtag();
@ -299,8 +303,10 @@ namespace Squidex.Areas.Api.Controllers.Apps
var userOrClientId = HttpContext.User.UserOrClientId()!; var userOrClientId = HttpContext.User.UserOrClientId()!;
var isFrontend = HttpContext.User.IsInClient(DefaultClients.Frontend);
var result = context.Result<IAppEntity>(); var result = context.Result<IAppEntity>();
var response = AppDto.FromApp(result, userOrClientId, appPlansProvider, Resources); var response = AppDto.FromApp(result, userOrClientId, isFrontend, appPlansProvider, Resources);
return response; return response;
} }

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

@ -16,6 +16,7 @@ using Squidex.Areas.Api.Controllers.Rules;
using Squidex.Areas.Api.Controllers.Schemas; using Squidex.Areas.Api.Controllers.Schemas;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.Plans; using Squidex.Domain.Apps.Entities.Apps.Plans;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Security;
using Squidex.Infrastructure.Validation; using Squidex.Infrastructure.Validation;
@ -73,6 +74,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// <summary> /// <summary>
/// Indicates if the user can access the api. /// Indicates if the user can access the api.
/// </summary> /// </summary>
[Obsolete("Usage role properties")]
public bool CanAccessApi { get; set; } public bool CanAccessApi { get; set; }
/// <summary> /// <summary>
@ -90,17 +92,30 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// </summary> /// </summary>
public string? PlanUpgrade { get; set; } 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()); var result = SimpleMapper.Map(app, new AppDto());
result.Permissions = permissions.ToIds(); 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)) if (resources.Includes(P.ForApp(P.AppContents, app.Name), permissions))
@ -108,17 +123,14 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
result.CanAccessContent = true; result.CanAccessContent = true;
} }
result.SetPlan(app, plans, resources, permissions);
result.SetImage(app, resources);
return result.CreateLinks(resources, permissions); 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>(); 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); permissions.AddRange(role.Permissions);
} }
@ -187,12 +199,12 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
AddGetLink("contributors", resources.Url<AppContributorsController>(x => nameof(x.GetContributors), values)); 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)); 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)); AddGetLink("patterns", resources.Url<AppPatternsController>(x => nameof(x.GetPatterns), values));
} }
@ -212,7 +224,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
AddGetLink("rules", resources.Url<RulesController>(x => nameof(x.GetRules), values)); 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)); 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. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Collections.Generic;
using System.Linq; using System.Linq;
using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Validation; using Squidex.Infrastructure.Validation;
using Squidex.Web; using Squidex.Web;
@ -41,18 +41,23 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// Associated list of permissions. /// Associated list of permissions.
/// </summary> /// </summary>
[LocalizedRequired] [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) public static RoleDto FromRole(Role role, IAppEntity app)
{ {
var permissions = role.Permissions;
var result = new RoleDto var result = new RoleDto
{ {
Name = role.Name, Name = role.Name,
NumClients = GetNumClients(role, app), NumClients = GetNumClients(role, app),
NumContributors = GetNumContributors(role, app), NumContributors = GetNumContributors(role, app),
Permissions = permissions.ToIds(), Permissions = role.Permissions.ToIds().ToArray(),
Properties = role.Properties,
IsDefaultRole = role.IsDefault 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. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Collections.Generic;
using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
@ -27,7 +26,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// <summary> /// <summary>
/// Optional fallback languages. /// Optional fallback languages.
/// </summary> /// </summary>
public List<Language>? Fallback { get; set; } public Language[]? Fallback { get; set; }
public UpdateLanguage ToCommand(Language language) 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.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Validation; using Squidex.Infrastructure.Validation;
namespace Squidex.Areas.Api.Controllers.Apps.Models namespace Squidex.Areas.Api.Controllers.Apps.Models
@ -18,9 +19,14 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
[LocalizedRequired] [LocalizedRequired]
public string[] Permissions { get; set; } public string[] Permissions { get; set; }
/// <summary>
/// Associated list of UI properties.
/// </summary>
public JsonObject? Properties { get; set; }
public UpdateRole ToCommand(string name) 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/Apps/Models/UpdateWorkflowDto.cs

@ -30,7 +30,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// <summary> /// <summary>
/// The schema ids. /// The schema ids.
/// </summary> /// </summary>
public List<Guid>? SchemaIds { get; set; } public IReadOnlyList<Guid>? SchemaIds { get; set; }
/// <summary> /// <summary>
/// The initial step. /// The initial step.

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

@ -88,7 +88,7 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models
public bool ForceResize { get; set; } public bool ForceResize { get; set; }
/// <summary> /// <summary>
/// True to force a new resize even if it already stored. /// The target image format.
/// </summary> /// </summary>
[FromQuery(Name = "format")] [FromQuery(Name = "format")]
public ImageFormat Format { get; set; } public ImageFormat Format { get; set; }

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

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

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

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

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

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

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

@ -6,7 +6,6 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Collections.Generic;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities;
@ -18,7 +17,7 @@ namespace Squidex.Areas.Api.Controllers
/// <summary> /// <summary>
/// The optional list of ids to query. /// The optional list of ids to query.
/// </summary> /// </summary>
public List<Guid>? Ids { get; set; } public Guid[]? Ids { get; set; }
/// <summary> /// <summary>
/// The optional odata query. /// 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. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Collections.Generic;
using System.Linq; using System.Linq;
using Squidex.Domain.Apps.Entities.Schemas.Commands; using Squidex.Domain.Apps.Entities.Schemas.Commands;
@ -16,13 +15,13 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
/// <summary> /// <summary>
/// The field rules to configure. /// The field rules to configure.
/// </summary> /// </summary>
public List<FieldRuleDto>? FieldRules { get; set; } public FieldRuleDto[]? FieldRules { get; set; }
public ConfigureFieldRules ToCommand() public ConfigureFieldRules ToCommand()
{ {
return new ConfigureFieldRules 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. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Collections.Generic;
using Squidex.Domain.Apps.Entities.Schemas.Commands; using Squidex.Domain.Apps.Entities.Schemas.Commands;
using Squidex.Infrastructure.Validation; using Squidex.Infrastructure.Validation;
@ -17,7 +16,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
/// The field ids in the target order. /// The field ids in the target order.
/// </summary> /// </summary>
[LocalizedRequired] [LocalizedRequired]
public List<long> FieldIds { get; set; } public long[] FieldIds { get; set; }
public ReorderFields ToCommand(long? parentId = null) 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> /// <summary>
/// The names of the fields that should be used in references. /// The names of the fields that should be used in references.
/// </summary> /// </summary>
public List<string>? FieldsInReferences { get; set; } public string[]? FieldsInReferences { get; set; }
/// <summary> /// <summary>
/// The names of the fields that should be shown in lists, including meta fields. /// The names of the fields that should be shown in lists, including meta fields.
/// </summary> /// </summary>
public List<string>? FieldsInLists { get; set; } public string[]? FieldsInLists { get; set; }
/// <summary> /// <summary>
/// Optional fields. /// Optional fields.
/// </summary> /// </summary>
public List<UpsertSchemaFieldDto?>? Fields { get; set; } public UpsertSchemaFieldDto[]? Fields { get; set; }
/// <summary> /// <summary>
/// The optional preview urls. /// The optional preview urls.
@ -82,9 +82,9 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
command.FieldsInReferences = new FieldNames(dto.FieldsInReferences); 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) foreach (var rootFieldDto in dto.Fields)
{ {
@ -95,9 +95,9 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
{ {
SimpleMapper.Map(rootFieldDto, rootField); 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) foreach (var nestedFieldDto in rootFieldDto.Nested)
{ {
@ -109,13 +109,17 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
SimpleMapper.Map(nestedFieldDto, nestedField); 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; 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. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Collections.Generic;
using Squidex.Infrastructure.Validation; using Squidex.Infrastructure.Validation;
namespace Squidex.Areas.Api.Controllers.Schemas.Models namespace Squidex.Areas.Api.Controllers.Schemas.Models
@ -48,6 +47,6 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
/// <summary> /// <summary>
/// The nested fields. /// The nested fields.
/// </summary> /// </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] [HttpGet]
[Route("apps/{app}/schemas/")] [Route("apps/{app}/schemas/")]
[ProducesResponseType(typeof(SchemasDto), 200)] [ProducesResponseType(typeof(SchemasDto), 200)]
[ApiPermissionOrAnonymous(Permissions.AppCommon)] [ApiPermissionOrAnonymous(Permissions.AppSchemasRead)]
[ApiCosts(0)] [ApiCosts(0)]
public async Task<IActionResult> GetSchemas(string app) public async Task<IActionResult> GetSchemas(string app)
{ {
@ -72,7 +72,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
[HttpGet] [HttpGet]
[Route("apps/{app}/schemas/{name}/")] [Route("apps/{app}/schemas/{name}/")]
[ProducesResponseType(typeof(SchemaDetailsDto), 200)] [ProducesResponseType(typeof(SchemaDetailsDto), 200)]
[ApiPermissionOrAnonymous(Permissions.AppCommon)] [ApiPermissionOrAnonymous(Permissions.AppSchemasRead)]
[ApiCosts(0)] [ApiCosts(0)]
public async Task<IActionResult> GetSchema(string app, string name) 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] [HttpGet]
[Route("apps/{app}/search/")] [Route("apps/{app}/search/")]
[ProducesResponseType(typeof(SearchResultDto[]), 200)] [ProducesResponseType(typeof(SearchResultDto[]), 200)]
[ApiPermissionOrAnonymous(Permissions.AppCommon)] [ApiPermissionOrAnonymous(Permissions.AppSearch)]
[ApiCosts(0)] [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); 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] [HttpGet]
[Route("apps/{app}/usages/log/")] [Route("apps/{app}/usages/log/")]
[ProducesResponseType(typeof(LogDownloadDto), 200)] [ProducesResponseType(typeof(LogDownloadDto), 200)]
[ApiPermissionOrAnonymous(Permissions.AppCommon)] [ApiPermissionOrAnonymous(Permissions.AppUsage)]
[ApiCosts(0)] [ApiCosts(0)]
public IActionResult GetLog(string app) public IActionResult GetLog(string app)
{ {
@ -93,7 +93,7 @@ namespace Squidex.Areas.Api.Controllers.Statistics
[HttpGet] [HttpGet]
[Route("apps/{app}/usages/calls/{fromDate}/{toDate}/")] [Route("apps/{app}/usages/calls/{fromDate}/{toDate}/")]
[ProducesResponseType(typeof(CallsUsageDtoDto), 200)] [ProducesResponseType(typeof(CallsUsageDtoDto), 200)]
[ApiPermissionOrAnonymous(Permissions.AppCommon)] [ApiPermissionOrAnonymous(Permissions.AppUsage)]
[ApiCosts(0)] [ApiCosts(0)]
public async Task<IActionResult> GetUsages(string app, DateTime fromDate, DateTime toDate) public async Task<IActionResult> GetUsages(string app, DateTime fromDate, DateTime toDate)
{ {
@ -122,7 +122,7 @@ namespace Squidex.Areas.Api.Controllers.Statistics
[HttpGet] [HttpGet]
[Route("apps/{app}/usages/storage/today/")] [Route("apps/{app}/usages/storage/today/")]
[ProducesResponseType(typeof(CurrentStorageDto), 200)] [ProducesResponseType(typeof(CurrentStorageDto), 200)]
[ApiPermissionOrAnonymous(Permissions.AppCommon)] [ApiPermissionOrAnonymous(Permissions.AppUsage)]
[ApiCosts(0)] [ApiCosts(0)]
public async Task<IActionResult> GetCurrentStorageSize(string app) public async Task<IActionResult> GetCurrentStorageSize(string app)
{ {
@ -149,7 +149,7 @@ namespace Squidex.Areas.Api.Controllers.Statistics
[HttpGet] [HttpGet]
[Route("apps/{app}/usages/storage/{fromDate}/{toDate}/")] [Route("apps/{app}/usages/storage/{fromDate}/{toDate}/")]
[ProducesResponseType(typeof(StorageUsagePerDateDto[]), 200)] [ProducesResponseType(typeof(StorageUsagePerDateDto[]), 200)]
[ApiPermissionOrAnonymous(Permissions.AppCommon)] [ApiPermissionOrAnonymous(Permissions.AppUsage)]
[ApiCosts(0)] [ApiCosts(0)]
public async Task<IActionResult> GetStorageSizes(string app, DateTime fromDate, DateTime toDate) 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] [HttpPost]
[Route("apps/{app}/translations/")] [Route("apps/{app}/translations/")]
[ProducesResponseType(typeof(TranslationDto), 200)] [ProducesResponseType(typeof(TranslationDto), 200)]
[ApiPermissionOrAnonymous(Permissions.AppCommon)] [ApiPermissionOrAnonymous(Permissions.AppTranslate)]
[ApiCosts(0)] [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 result = await translator.Translate(request.Text, request.TargetLanguage, request.SourceLanguage, HttpContext.RequestAborted);
var response = TranslationDto.FromTranslation(result); var response = TranslationDto.FromTranslation(result);

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

@ -47,6 +47,7 @@ namespace Squidex.Config.Domain
new NamedStringIdConverter(), new NamedStringIdConverter(),
new PropertyPathConverter(), new PropertyPathConverter(),
new RefTokenConverter(), new RefTokenConverter(),
new RoleConverter(),
new RolesConverter(), new RolesConverter(),
new RuleConverter(), new RuleConverter(),
new SchemaConverter(), 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] [Fact]
public void Should_be_default_role() public void Should_be_default_role()
{ {
var role = new Role("Owner"); var role = Role.Create("Owner");
Assert.True(role.IsDefault); Assert.True(role.IsDefault);
} }
@ -24,25 +24,25 @@ namespace Squidex.Domain.Apps.Core.Model.Apps
[Fact] [Fact]
public void Should_not_be_default_role() public void Should_not_be_default_role()
{ {
var role = new Role("Custom"); var role = Role.Create("Custom");
Assert.False(role.IsDefault); Assert.False(role.IsDefault);
} }
[Fact] [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(); var result = role.ForApp("my-app").Permissions.ToIds();
Assert.Equal(new[] { "squidex.apps.my-app.common" }, result); Assert.Empty(result);
} }
[Fact] [Fact]
public void Should_not_have_duplicate_permission() 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(); var result = role.ForApp("my-app").Permissions.ToIds();
@ -50,19 +50,19 @@ namespace Squidex.Domain.Apps.Core.Model.Apps
} }
[Fact] [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(); 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] [Fact]
public void Should_check_for_name() public void Should_check_for_name()
{ {
var role = new Role("Custom"); var role = Role.WithPermissions("Custom");
Assert.True(role.Equals("Custom")); Assert.True(role.Equals("Custom"));
} }
@ -70,7 +70,7 @@ namespace Squidex.Domain.Apps.Core.Model.Apps
[Fact] [Fact]
public void Should_check_for_null_name() 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((string)null!));
Assert.False(role.Equals("Other")); 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. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Collections.Generic;
using FluentAssertions; using FluentAssertions;
using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.TestHelpers; using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Security;
using Xunit; using Xunit;
namespace Squidex.Domain.Apps.Core.Model.Apps namespace Squidex.Domain.Apps.Core.Model.Apps
{ {
public class RolesJsonTests public class RolesJsonTests
{ {
[Fact]
public void Should_deserialize_from_old_role_format()
{
var source = new Dictionary<string, string[]>
{
["Custom"] = new string[]
{
"Permission1",
"Permission2"
}
};
var expected =
Roles.Empty
.Add("Custom")
.Update("Custom",
new PermissionSet(
"Permission1",
"Permission2"));
var roles = source.SerializeAndDeserialize<Roles>();
roles.Should().BeEquivalentTo(expected);
}
[Fact] [Fact]
public void Should_serialize_and_deserialize() 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(); 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 System.Linq;
using FluentAssertions; using FluentAssertions;
using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Apps;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Security;
using Xunit; using Xunit;
@ -40,7 +41,7 @@ namespace Squidex.Domain.Apps.Core.Model.Apps
{ {
var roles_1 = roles_0.Add(role); 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] [Fact]
@ -61,15 +62,23 @@ namespace Squidex.Domain.Apps.Core.Model.Apps
} }
[Fact] [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] [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); var roles_1 = roles_0.Update(firstRole);
@ -79,7 +88,7 @@ namespace Squidex.Domain.Apps.Core.Model.Apps
[Fact] [Fact]
public void Should_return_same_roles_if_role_not_found() 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); Assert.Same(roles_0, roles_1);
} }
@ -142,14 +151,14 @@ namespace Squidex.Domain.Apps.Core.Model.Apps
Assert.False(Roles.IsDefault(firstRole)); Assert.False(Roles.IsDefault(firstRole));
} }
[InlineData("Developer")] [InlineData("Developer", 7)]
[InlineData("Editor")] [InlineData("Editor", 4)]
[InlineData("Owner")] [InlineData("Reader", 2)]
[InlineData("Reader")] [InlineData("Owner", 1)]
[Theory] [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(found);
Assert.True(result!.IsDefault); Assert.True(result!.IsDefault);
@ -158,13 +167,29 @@ namespace Squidex.Domain.Apps.Core.Model.Apps
foreach (var permission in result.Permissions) foreach (var permission in result.Permissions)
{ {
Assert.StartsWith("squidex.apps.app.", permission.Id); 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] [Fact]
public void Should_return_null_if_role_not_found() 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.False(found);
Assert.Null(result); Assert.Null(result);

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

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

1
backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TestUtils.cs

@ -59,6 +59,7 @@ namespace Squidex.Domain.Apps.Core.TestHelpers
new NamedStringIdConverter(), new NamedStringIdConverter(),
new PropertyPathConverter(), new PropertyPathConverter(),
new RefTokenConverter(), new RefTokenConverter(),
new RoleConverter(),
new RolesConverter(), new RolesConverter(),
new RuleConverter(), new RuleConverter(),
new SchemaConverter(), new SchemaConverter(),

10
backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs

@ -6,7 +6,6 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using FakeItEasy; using FakeItEasy;
using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Apps;
@ -18,6 +17,7 @@ using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Domain.Apps.Events.Apps; using Squidex.Domain.Apps.Events.Apps;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Log;
using Squidex.Shared.Users; using Squidex.Shared.Users;
using Xunit; using Xunit;
@ -529,7 +529,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
[Fact] [Fact]
public async Task UpdateLanguage_should_create_events_and_update_language() public async Task UpdateLanguage_should_create_events_and_update_language()
{ {
var command = new UpdateLanguage { Language = Language.DE, Fallback = new List<Language> { Language.EN } }; var command = new UpdateLanguage { Language = Language.DE, Fallback = new[] { Language.EN } };
await ExecuteCreateAsync(); await ExecuteCreateAsync();
await ExecuteAddLanguageAsync(Language.DE); await ExecuteAddLanguageAsync(Language.DE);
@ -542,7 +542,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
LastEvents LastEvents
.ShouldHaveSameEvents( .ShouldHaveSameEvents(
CreateEvent(new AppLanguageUpdated { Language = Language.DE, Fallback = new List<Language> { Language.EN } }) CreateEvent(new AppLanguageUpdated { Language = Language.DE, Fallback = new[] { Language.EN } })
); );
} }
@ -588,7 +588,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
[Fact] [Fact]
public async Task UpdateRole_should_create_events_and_update_role() public async Task UpdateRole_should_create_events_and_update_role()
{ {
var command = new UpdateRole { Name = roleName, Permissions = new[] { "clients.read" } }; var command = new UpdateRole { Name = roleName, Permissions = new[] { "clients.read" }, Properties = JsonValue.Object() };
await ExecuteCreateAsync(); await ExecuteCreateAsync();
await ExecuteAddRoleAsync(); await ExecuteAddRoleAsync();
@ -599,7 +599,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
LastEvents LastEvents
.ShouldHaveSameEvents( .ShouldHaveSameEvents(
CreateEvent(new AppRoleUpdated { Name = roleName, Permissions = new[] { "clients.read" } }) CreateEvent(new AppRoleUpdated { Name = roleName, Permissions = command.Permissions, Properties = command.Properties })
); );
} }

79
backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppSettingsSearchSourceTests.cs

@ -32,7 +32,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
} }
[Fact] [Fact]
public async Task Should_empty_if_nothing_matching() public async Task Should_return_empty_if_nothing_matching()
{ {
var ctx = ContextWithPermission(); var ctx = ContextWithPermission();
@ -42,39 +42,38 @@ namespace Squidex.Domain.Apps.Entities.Apps
} }
[Fact] [Fact]
public async Task Should_always_return_contents_result_if_matching() public async Task Should_return_dashboard_result_if_matching_and_permission_given()
{ {
var ctx = ContextWithPermission(); var permission = Permissions.ForApp(Permissions.AppUsage, appId.Name);
var ctx = ContextWithPermission(permission.Id);
A.CallTo(() => urlGenerator.ContentsUI(appId)) A.CallTo(() => urlGenerator.DashboardUI(appId))
.Returns("contents-url"); .Returns("dashboard-url");
var result = await sut.SearchAsync("content", ctx); var result = await sut.SearchAsync("dashboard", ctx);
result.Should().BeEquivalentTo( result.Should().BeEquivalentTo(
new SearchResults() new SearchResults()
.Add("Contents", SearchResultType.Content, "contents-url")); .Add("Dashboard", SearchResultType.Dashboard, "dashboard-url"));
} }
[Fact] [Fact]
public async Task Should_always_return_dashboard_result_if_matching() public async Task Should_not_return_dashboard_result_if_user_has_no_permission()
{ {
var ctx = ContextWithPermission(); var ctx = ContextWithPermission();
A.CallTo(() => urlGenerator.DashboardUI(appId)) var result = await sut.SearchAsync("assets", ctx);
.Returns("dashboard-url");
var result = await sut.SearchAsync("dashboard", ctx);
result.Should().BeEquivalentTo( Assert.Empty(result);
new SearchResults()
.Add("Dashboard", SearchResultType.Dashboard, "dashboard-url"));
} }
[Fact] [Fact]
public async Task Should_always_return_languages_result_if_matching() public async Task Should_return_languages_result_if_matching_and_permission_given()
{ {
var ctx = ContextWithPermission(null); var permission = Permissions.ForApp(Permissions.AppLanguagesRead, appId.Name);
var ctx = ContextWithPermission(permission.Id);
A.CallTo(() => urlGenerator.LanguagesUI(appId)) A.CallTo(() => urlGenerator.LanguagesUI(appId))
.Returns("languages-url"); .Returns("languages-url");
@ -87,10 +86,22 @@ namespace Squidex.Domain.Apps.Entities.Apps
} }
[Fact] [Fact]
public async Task Should_always_return_patterns_result_if_matching() public async Task Should_not_return_languages_result_if_user_has_no_permission()
{ {
var ctx = ContextWithPermission(); var ctx = ContextWithPermission();
var result = await sut.SearchAsync("assets", ctx);
Assert.Empty(result);
}
[Fact]
public async Task Should_return_patterns_result_if_matching_and_permission_given()
{
var permission = Permissions.ForApp(Permissions.AppPatternsRead, appId.Name);
var ctx = ContextWithPermission(permission.Id);
A.CallTo(() => urlGenerator.PatternsUI(appId)) A.CallTo(() => urlGenerator.PatternsUI(appId))
.Returns("patterns-url"); .Returns("patterns-url");
@ -102,10 +113,22 @@ namespace Squidex.Domain.Apps.Entities.Apps
} }
[Fact] [Fact]
public async Task Should_always_return_schemas_result_if_matching() public async Task Should_not_return_patterns_result_if_user_has_no_permission()
{ {
var ctx = ContextWithPermission(); var ctx = ContextWithPermission();
var result = await sut.SearchAsync("patterns", ctx);
Assert.Empty(result);
}
[Fact]
public async Task Should_return_schemas_result_if_matching_and_permission_given()
{
var permission = Permissions.ForApp(Permissions.AppSchemasRead, appId.Name);
var ctx = ContextWithPermission(permission.Id);
A.CallTo(() => urlGenerator.SchemasUI(appId)) A.CallTo(() => urlGenerator.SchemasUI(appId))
.Returns("schemas-url"); .Returns("schemas-url");
@ -116,6 +139,16 @@ namespace Squidex.Domain.Apps.Entities.Apps
.Add("Schemas", SearchResultType.Schema, "schemas-url")); .Add("Schemas", SearchResultType.Schema, "schemas-url"));
} }
[Fact]
public async Task Should_not_return_schemas_result_if_user_has_no_permission()
{
var ctx = ContextWithPermission();
var result = await sut.SearchAsync("schemas", ctx);
Assert.Empty(result);
}
[Fact] [Fact]
public async Task Should_return_assets_result_if_matching_and_permission_given() public async Task Should_return_assets_result_if_matching_and_permission_given()
{ {
@ -323,7 +356,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
} }
[Fact] [Fact]
public async Task Should_not_workflows_clients_result_if_user_has_no_permission() public async Task Should_not_return_workflows_result_if_user_has_no_permission()
{ {
var ctx = ContextWithPermission(); var ctx = ContextWithPermission();
@ -337,13 +370,11 @@ namespace Squidex.Domain.Apps.Entities.Apps
var claimsIdentity = new ClaimsIdentity(); var claimsIdentity = new ClaimsIdentity();
var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
if (permission == null) if (permission != null)
{ {
permission = Permissions.ForApp(Permissions.AppCommon, appId.Name).Id; claimsIdentity.AddClaim(new Claim(SquidexClaimTypes.Permissions, permission));
} }
claimsIdentity.AddClaim(new Claim(SquidexClaimTypes.Permissions, permission));
return new Context(claimsPrincipal, Mocks.App(appId)); return new Context(claimsPrincipal, Mocks.App(appId));
} }
} }

7
backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppLanguagesTests.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Collections.Generic;
using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.TestHelpers; using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Domain.Apps.Entities.Apps.Commands;
@ -101,7 +100,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
[Fact] [Fact]
public void CanUpdateLanguage_should_throw_exception_if_fallback_language_defined_and_master() public void CanUpdateLanguage_should_throw_exception_if_fallback_language_defined_and_master()
{ {
var command = new UpdateLanguage { Language = Language.EN, Fallback = new List<Language> { Language.DE } }; var command = new UpdateLanguage { Language = Language.EN, Fallback = new[] { Language.DE } };
ValidationAssert.Throws(() => GuardAppLanguages.CanUpdate(languages, command), ValidationAssert.Throws(() => GuardAppLanguages.CanUpdate(languages, command),
new ValidationError("Master language cannot have fallback languages.", "Fallback")); new ValidationError("Master language cannot have fallback languages.", "Fallback"));
@ -110,7 +109,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
[Fact] [Fact]
public void CanUpdateLanguage_should_throw_exception_if_language_has_invalid_fallback() public void CanUpdateLanguage_should_throw_exception_if_language_has_invalid_fallback()
{ {
var command = new UpdateLanguage { Language = Language.DE, Fallback = new List<Language> { Language.IT } }; var command = new UpdateLanguage { Language = Language.DE, Fallback = new[] { Language.IT } };
ValidationAssert.Throws(() => GuardAppLanguages.CanUpdate(languages, command), ValidationAssert.Throws(() => GuardAppLanguages.CanUpdate(languages, command),
new ValidationError("App does not have fallback language 'Italian'.", "Fallback")); new ValidationError("App does not have fallback language 'Italian'.", "Fallback"));
@ -127,7 +126,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
[Fact] [Fact]
public void CanUpdateLanguage_should_not_throw_exception_if_language_is_valid() public void CanUpdateLanguage_should_not_throw_exception_if_language_is_valid()
{ {
var command = new UpdateLanguage { Language = Language.DE, Fallback = new List<Language> { Language.EN } }; var command = new UpdateLanguage { Language = Language.DE, Fallback = new[] { Language.EN } };
GuardAppLanguages.CanUpdate(languages, command); GuardAppLanguages.CanUpdate(languages, command);
} }

17
backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppRolesTests.cs

@ -10,6 +10,7 @@ using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Validation; using Squidex.Infrastructure.Validation;
using Xunit; using Xunit;
@ -85,9 +86,11 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
{ {
var roles_1 = roles_0.Add(roleName); var roles_1 = roles_0.Add(roleName);
var clients_1 = clients.Add("1", new AppClient("client", "1", roleName));
var command = new DeleteRole { Name = roleName }; var command = new DeleteRole { Name = roleName };
ValidationAssert.Throws(() => GuardAppRoles.CanDelete(roles_1, command, contributors, clients.Add("1", new AppClient("client", "1", roleName))), ValidationAssert.Throws(() => GuardAppRoles.CanDelete(roles_1, command, contributors, clients_1),
new ValidationError("Cannot remove a role when a client is assigned.")); new ValidationError("Cannot remove a role when a client is assigned."));
} }
@ -128,7 +131,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
{ {
var roles_1 = roles_0.Add(roleName); var roles_1 = roles_0.Add(roleName);
var command = new UpdateRole { Name = roleName, Permissions = null! }; var command = new UpdateRole { Name = roleName };
ValidationAssert.Throws(() => GuardAppRoles.CanUpdate(roles_1, command), ValidationAssert.Throws(() => GuardAppRoles.CanUpdate(roles_1, command),
new ValidationError("Permissions is required.", "Permissions")); new ValidationError("Permissions is required.", "Permissions"));
@ -153,6 +156,16 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
Assert.Throws<DomainObjectNotFoundException>(() => GuardAppRoles.CanUpdate(roles_0, command)); Assert.Throws<DomainObjectNotFoundException>(() => GuardAppRoles.CanUpdate(roles_0, command));
} }
[Fact]
public void CanUpdate_should_not_throw_exception_if_properties_is_null()
{
var roles_1 = roles_0.Add(roleName);
var command = new UpdateRole { Name = roleName, Permissions = new[] { "P1" } };
GuardAppRoles.CanUpdate(roles_1, command);
}
[Fact] [Fact]
public void CanUpdate_should_not_throw_exception_if_role_exist_with_valid_command() public void CanUpdate_should_not_throw_exception_if_role_exist_with_valid_command()
{ {

21
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/BulkUpdateCommandMiddlewareTests.cs

@ -6,7 +6,6 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using FakeItEasy; using FakeItEasy;
@ -56,7 +55,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
[Fact] [Fact]
public async Task Should_do_nothing_if_jobs_is_empty() public async Task Should_do_nothing_if_jobs_is_empty()
{ {
var command = new BulkUpdateContents { Jobs = new List<BulkUpdateJob>() }; var command = new BulkUpdateContents { Jobs = Array.Empty<BulkUpdateJob>() };
var context = new CommandContext(command, commandBus); var context = new CommandContext(command, commandBus);
@ -80,7 +79,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
var command = new BulkUpdateContents var command = new BulkUpdateContents
{ {
Jobs = new List<BulkUpdateJob> Jobs = new[]
{ {
new BulkUpdateJob new BulkUpdateJob
{ {
@ -119,7 +118,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
var command = new BulkUpdateContents var command = new BulkUpdateContents
{ {
Jobs = new List<BulkUpdateJob> Jobs = new[]
{ {
new BulkUpdateJob new BulkUpdateJob
{ {
@ -154,7 +153,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
var command = new BulkUpdateContents var command = new BulkUpdateContents
{ {
Jobs = new List<BulkUpdateJob> Jobs = new[]
{ {
new BulkUpdateJob new BulkUpdateJob
{ {
@ -189,7 +188,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
var command = new BulkUpdateContents var command = new BulkUpdateContents
{ {
Jobs = new List<BulkUpdateJob> Jobs = new[]
{ {
new BulkUpdateJob new BulkUpdateJob
{ {
@ -224,7 +223,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
var command = new BulkUpdateContents var command = new BulkUpdateContents
{ {
Jobs = new List<BulkUpdateJob> Jobs = new[]
{ {
new BulkUpdateJob new BulkUpdateJob
{ {
@ -256,7 +255,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
var command = new BulkUpdateContents var command = new BulkUpdateContents
{ {
Jobs = new List<BulkUpdateJob> Jobs = new[]
{ {
new BulkUpdateJob new BulkUpdateJob
{ {
@ -287,7 +286,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
var command = new BulkUpdateContents var command = new BulkUpdateContents
{ {
Jobs = new List<BulkUpdateJob> Jobs = new[]
{ {
new BulkUpdateJob new BulkUpdateJob
{ {
@ -318,7 +317,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
var command = new BulkUpdateContents var command = new BulkUpdateContents
{ {
Jobs = new List<BulkUpdateJob> Jobs = new[]
{ {
new BulkUpdateJob new BulkUpdateJob
{ {
@ -349,7 +348,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
var command = new BulkUpdateContents var command = new BulkUpdateContents
{ {
Jobs = new List<BulkUpdateJob> Jobs = new[]
{ {
new BulkUpdateJob new BulkUpdateJob
{ {

52
backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaTests.cs

@ -49,7 +49,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
var command = new CreateSchema var command = new CreateSchema
{ {
AppId = appId, AppId = appId,
Fields = new List<UpsertSchemaField> Fields = new[]
{ {
new UpsertSchemaField new UpsertSchemaField
{ {
@ -72,7 +72,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
var command = new CreateSchema var command = new CreateSchema
{ {
AppId = appId, AppId = appId,
Fields = new List<UpsertSchemaField> Fields = new[]
{ {
new UpsertSchemaField new UpsertSchemaField
{ {
@ -95,7 +95,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
var command = new CreateSchema var command = new CreateSchema
{ {
AppId = appId, AppId = appId,
Fields = new List<UpsertSchemaField> Fields = new[]
{ {
new UpsertSchemaField new UpsertSchemaField
{ {
@ -119,7 +119,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
var command = new CreateSchema var command = new CreateSchema
{ {
AppId = appId, AppId = appId,
Fields = new List<UpsertSchemaField> Fields = new[]
{ {
new UpsertSchemaField new UpsertSchemaField
{ {
@ -142,7 +142,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
var command = new CreateSchema var command = new CreateSchema
{ {
AppId = appId, AppId = appId,
Fields = new List<UpsertSchemaField> Fields = new[]
{ {
new UpsertSchemaField new UpsertSchemaField
{ {
@ -171,14 +171,14 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
var command = new CreateSchema var command = new CreateSchema
{ {
AppId = appId, AppId = appId,
Fields = new List<UpsertSchemaField> Fields = new[]
{ {
new UpsertSchemaField new UpsertSchemaField
{ {
Name = "array", Name = "array",
Properties = new ArrayFieldProperties(), Properties = new ArrayFieldProperties(),
Partitioning = Partitioning.Invariant.Key, Partitioning = Partitioning.Invariant.Key,
Nested = new List<UpsertSchemaNestedField> Nested = new[]
{ {
new UpsertSchemaNestedField new UpsertSchemaNestedField
{ {
@ -202,14 +202,14 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
var command = new CreateSchema var command = new CreateSchema
{ {
AppId = appId, AppId = appId,
Fields = new List<UpsertSchemaField> Fields = new[]
{ {
new UpsertSchemaField new UpsertSchemaField
{ {
Name = "array", Name = "array",
Properties = new ArrayFieldProperties(), Properties = new ArrayFieldProperties(),
Partitioning = Partitioning.Invariant.Key, Partitioning = Partitioning.Invariant.Key,
Nested = new List<UpsertSchemaNestedField> Nested = new[]
{ {
new UpsertSchemaNestedField new UpsertSchemaNestedField
{ {
@ -233,14 +233,14 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
var command = new CreateSchema var command = new CreateSchema
{ {
AppId = appId, AppId = appId,
Fields = new List<UpsertSchemaField> Fields = new[]
{ {
new UpsertSchemaField new UpsertSchemaField
{ {
Name = "array", Name = "array",
Properties = new ArrayFieldProperties(), Properties = new ArrayFieldProperties(),
Partitioning = Partitioning.Invariant.Key, Partitioning = Partitioning.Invariant.Key,
Nested = new List<UpsertSchemaNestedField> Nested = new[]
{ {
new UpsertSchemaNestedField new UpsertSchemaNestedField
{ {
@ -264,14 +264,14 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
var command = new CreateSchema var command = new CreateSchema
{ {
AppId = appId, AppId = appId,
Fields = new List<UpsertSchemaField> Fields = new[]
{ {
new UpsertSchemaField new UpsertSchemaField
{ {
Name = "array", Name = "array",
Properties = new ArrayFieldProperties(), Properties = new ArrayFieldProperties(),
Partitioning = Partitioning.Invariant.Key, Partitioning = Partitioning.Invariant.Key,
Nested = new List<UpsertSchemaNestedField> Nested = new[]
{ {
new UpsertSchemaNestedField new UpsertSchemaNestedField
{ {
@ -296,14 +296,14 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
var command = new CreateSchema var command = new CreateSchema
{ {
AppId = appId, AppId = appId,
Fields = new List<UpsertSchemaField> Fields = new[]
{ {
new UpsertSchemaField new UpsertSchemaField
{ {
Name = "array", Name = "array",
Properties = new ArrayFieldProperties(), Properties = new ArrayFieldProperties(),
Partitioning = Partitioning.Invariant.Key, Partitioning = Partitioning.Invariant.Key,
Nested = new List<UpsertSchemaNestedField> Nested = new[]
{ {
new UpsertSchemaNestedField new UpsertSchemaNestedField
{ {
@ -332,7 +332,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
var command = new CreateSchema var command = new CreateSchema
{ {
AppId = appId, AppId = appId,
Fields = new List<UpsertSchemaField> Fields = new[]
{ {
new UpsertSchemaField new UpsertSchemaField
{ {
@ -364,7 +364,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
{ {
var command = new CreateSchema var command = new CreateSchema
{ {
Fields = new List<UpsertSchemaField> Fields = new[]
{ {
new UpsertSchemaField new UpsertSchemaField
{ {
@ -402,7 +402,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
{ {
var command = new CreateSchema var command = new CreateSchema
{ {
Fields = new List<UpsertSchemaField> Fields = new[]
{ {
new UpsertSchemaField new UpsertSchemaField
{ {
@ -456,7 +456,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
var command = new CreateSchema var command = new CreateSchema
{ {
AppId = appId, AppId = appId,
Fields = new List<UpsertSchemaField> Fields = new[]
{ {
new UpsertSchemaField new UpsertSchemaField
{ {
@ -477,7 +477,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
Name = "field3", Name = "field3",
Properties = new ArrayFieldProperties(), Properties = new ArrayFieldProperties(),
Partitioning = Partitioning.Invariant.Key, Partitioning = Partitioning.Invariant.Key,
Nested = new List<UpsertSchemaNestedField> Nested = new[]
{ {
new UpsertSchemaNestedField new UpsertSchemaNestedField
{ {
@ -575,7 +575,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
{ {
var command = new ConfigureFieldRules var command = new ConfigureFieldRules
{ {
FieldRules = new List<FieldRuleCommand> FieldRules = new[]
{ {
new FieldRuleCommand { Field = "field", Action = (FieldRuleAction)5 }, new FieldRuleCommand { Field = "field", Action = (FieldRuleAction)5 },
new FieldRuleCommand(), new FieldRuleCommand(),
@ -594,7 +594,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
{ {
var command = new ConfigureFieldRules var command = new ConfigureFieldRules
{ {
FieldRules = new List<FieldRuleCommand> FieldRules = new[]
{ {
new FieldRuleCommand { Field = "field1", Action = FieldRuleAction.Disable, Condition = "a == b" }, new FieldRuleCommand { Field = "field1", Action = FieldRuleAction.Disable, Condition = "a == b" },
new FieldRuleCommand { Field = "field2" } new FieldRuleCommand { Field = "field2" }
@ -634,7 +634,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
[Fact] [Fact]
public void CanReorder_should_throw_exception_if_field_ids_contains_invalid_id() public void CanReorder_should_throw_exception_if_field_ids_contains_invalid_id()
{ {
var command = new ReorderFields { FieldIds = new List<long> { 1, 3 } }; var command = new ReorderFields { FieldIds = new[] { 1L, 3L } };
ValidationAssert.Throws(() => GuardSchema.CanReorder(command, schema_0), ValidationAssert.Throws(() => GuardSchema.CanReorder(command, schema_0),
new ValidationError("Field ids do not cover all fields.", "FieldIds")); new ValidationError("Field ids do not cover all fields.", "FieldIds"));
@ -643,7 +643,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
[Fact] [Fact]
public void CanReorder_should_throw_exception_if_field_ids_do_not_covers_all_fields() public void CanReorder_should_throw_exception_if_field_ids_do_not_covers_all_fields()
{ {
var command = new ReorderFields { FieldIds = new List<long> { 1 } }; var command = new ReorderFields { FieldIds = new[] { 1L } };
ValidationAssert.Throws(() => GuardSchema.CanReorder(command, schema_0), ValidationAssert.Throws(() => GuardSchema.CanReorder(command, schema_0),
new ValidationError("Field ids do not cover all fields.", "FieldIds")); new ValidationError("Field ids do not cover all fields.", "FieldIds"));
@ -661,7 +661,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
[Fact] [Fact]
public void CanReorder_should_throw_exception_if_parent_field_not_found() public void CanReorder_should_throw_exception_if_parent_field_not_found()
{ {
var command = new ReorderFields { FieldIds = new List<long> { 1, 2 }, ParentFieldId = 99 }; var command = new ReorderFields { FieldIds = new[] { 1L, 2L }, ParentFieldId = 99 };
Assert.Throws<DomainObjectNotFoundException>(() => GuardSchema.CanReorder(command, schema_0)); Assert.Throws<DomainObjectNotFoundException>(() => GuardSchema.CanReorder(command, schema_0));
} }
@ -669,7 +669,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
[Fact] [Fact]
public void CanReorder_should_not_throw_exception_if_field_ids_are_valid() public void CanReorder_should_not_throw_exception_if_field_ids_are_valid()
{ {
var command = new ReorderFields { FieldIds = new List<long> { 1, 2, 4 } }; var command = new ReorderFields { FieldIds = new[] { 1L, 2L, 4L } };
GuardSchema.CanReorder(command, schema_0); GuardSchema.CanReorder(command, schema_0);
} }

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaCommandsTests.cs

@ -23,7 +23,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
{ {
IsPublished = true, IsPublished = true,
Properties = new SchemaProperties { Hints = "MyHints" }, Properties = new SchemaProperties { Hints = "MyHints" },
Fields = new List<UpsertSchemaField> Fields = new[]
{ {
new UpsertSchemaField new UpsertSchemaField
{ {

10
backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaDomainObjectTests.cs

@ -80,7 +80,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
{ {
var properties = new SchemaProperties(); var properties = new SchemaProperties();
var fields = new List<UpsertSchemaField> var fields = new[]
{ {
new UpsertSchemaField { Name = "field1", Properties = ValidProperties() }, new UpsertSchemaField { Name = "field1", Properties = ValidProperties() },
new UpsertSchemaField { Name = "field2", Properties = ValidProperties() }, new UpsertSchemaField { Name = "field2", Properties = ValidProperties() },
@ -89,7 +89,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
Name = "field3", Name = "field3",
Partitioning = Partitioning.Language.Key, Partitioning = Partitioning.Language.Key,
Properties = new ArrayFieldProperties(), Properties = new ArrayFieldProperties(),
Nested = new List<UpsertSchemaNestedField> Nested = new[]
{ {
new UpsertSchemaNestedField { Name = "nested1", Properties = ValidProperties() }, new UpsertSchemaNestedField { Name = "nested1", Properties = ValidProperties() },
new UpsertSchemaNestedField { Name = "nested2", Properties = ValidProperties() } new UpsertSchemaNestedField { Name = "nested2", Properties = ValidProperties() }
@ -161,7 +161,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
{ {
var command = new ConfigureFieldRules var command = new ConfigureFieldRules
{ {
FieldRules = new List<FieldRuleCommand> FieldRules = new[]
{ {
new FieldRuleCommand { Field = "field1" } new FieldRuleCommand { Field = "field1" }
} }
@ -332,7 +332,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
[Fact] [Fact]
public async Task Reorder_should_create_events_and_reorder_fields() public async Task Reorder_should_create_events_and_reorder_fields()
{ {
var command = new ReorderFields { FieldIds = new List<long> { 2, 1 } }; var command = new ReorderFields { FieldIds = new[] { 2L, 1L } };
await ExecuteCreateAsync(); await ExecuteCreateAsync();
await ExecuteAddFieldAsync("field1"); await ExecuteAddFieldAsync("field1");
@ -351,7 +351,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
[Fact] [Fact]
public async Task Reorder_should_create_events_and_reorder_nestedy_fields() public async Task Reorder_should_create_events_and_reorder_nestedy_fields()
{ {
var command = new ReorderFields { ParentFieldId = 1, FieldIds = new List<long> { 3, 2 } }; var command = new ReorderFields { ParentFieldId = 1, FieldIds = new[] { 3L, 2L } };
await ExecuteCreateAsync(); await ExecuteCreateAsync();
await ExecuteAddArrayFieldAsync(); await ExecuteAddArrayFieldAsync();

81
backend/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Security.Claims; using System.Security.Claims;
@ -20,6 +21,7 @@ using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Security;
using Squidex.Shared;
using Squidex.Shared.Identity; using Squidex.Shared.Identity;
using Xunit; using Xunit;
@ -94,7 +96,7 @@ namespace Squidex.Web.Pipeline
{ {
var user = SetupUser(); var user = SetupUser();
var app = CreateApp(appName, appUser: "user1"); var app = CreateApp(appName);
user.AddClaim(new Claim(OpenIdClaims.Subject, "user1")); user.AddClaim(new Claim(OpenIdClaims.Subject, "user1"));
user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.my-app")); user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.my-app"));
@ -104,8 +106,59 @@ namespace Squidex.Web.Pipeline
await sut.OnActionExecutionAsync(actionExecutingContext, next); await sut.OnActionExecutionAsync(actionExecutingContext, next);
var permissions = user.Claims.Where(x => x.Type == SquidexClaimTypes.Permissions).ToList();
Assert.Same(app, httpContext.Context().App);
Assert.True(user.Claims.Any());
Assert.True(permissions.Count < 3);
Assert.True(permissions.All(x => x.Value.StartsWith("squidex.apps.my-app", StringComparison.OrdinalIgnoreCase)));
Assert.True(isNextCalled);
}
[Fact]
public async Task Should_resolve_app_from_contributor()
{
var user = SetupUser();
var app = CreateApp(appName, appUser: "user1");
user.AddClaim(new Claim(OpenIdClaims.Subject, "user1"));
A.CallTo(() => appProvider.GetAppAsync(appName, true))
.Returns(app);
await sut.OnActionExecutionAsync(actionExecutingContext, next);
var permissions = user.Claims.Where(x => x.Type == SquidexClaimTypes.Permissions).ToList();
Assert.Same(app, httpContext.Context().App);
Assert.True(user.Claims.Count() > 2);
Assert.True(permissions.Count < 3);
Assert.True(permissions.All(x => x.Value.StartsWith("squidex.apps.my-app", StringComparison.OrdinalIgnoreCase)));
Assert.True(isNextCalled);
}
[Fact]
public async Task Should_provide_extra_permissions_if_client_is_frontend()
{
var user = SetupUser();
var app = CreateApp(appName, appUser: "user1");
user.AddClaim(new Claim(OpenIdClaims.Subject, "user1"));
user.AddClaim(new Claim(OpenIdClaims.ClientId, DefaultClients.Frontend));
A.CallTo(() => appProvider.GetAppAsync(appName, false))
.Returns(app);
await sut.OnActionExecutionAsync(actionExecutingContext, next);
var permissions = user.Claims.Where(x => x.Type == SquidexClaimTypes.Permissions).ToList();
Assert.Same(app, httpContext.Context().App); Assert.Same(app, httpContext.Context().App);
Assert.True(user.Claims.Count() > 2); Assert.True(user.Claims.Count() > 2);
Assert.True(permissions.Count > 10);
Assert.True(permissions.All(x => x.Value.StartsWith("squidex.apps.my-app", StringComparison.OrdinalIgnoreCase)));
Assert.True(isNextCalled); Assert.True(isNextCalled);
} }
@ -231,28 +284,26 @@ namespace Squidex.Web.Pipeline
{ {
var appEntity = A.Fake<IAppEntity>(); var appEntity = A.Fake<IAppEntity>();
var contributors = AppContributors.Empty;
if (appUser != null) if (appUser != null)
{ {
A.CallTo(() => appEntity.Contributors) contributors = contributors.Assign(appUser, Role.Reader);
.Returns(AppContributors.Empty.Assign(appUser, Role.Owner));
}
else
{
A.CallTo(() => appEntity.Contributors)
.Returns(AppContributors.Empty);
} }
var clients = AppClients.Empty;
if (appClient != null) if (appClient != null)
{ {
A.CallTo(() => appEntity.Clients) clients = clients.Add(appClient, "secret").Update(appClient, apiCallsLimit: apiCallsLimit, allowAnonymous: allowAnonymous);
.Returns(AppClients.Empty.Add(appClient, "secret").Update(appClient, apiCallsLimit: apiCallsLimit, allowAnonymous: allowAnonymous));
}
else
{
A.CallTo(() => appEntity.Clients)
.Returns(AppClients.Empty);
} }
A.CallTo(() => appEntity.Contributors)
.Returns(contributors);
A.CallTo(() => appEntity.Clients)
.Returns(clients);
A.CallTo(() => appEntity.Name) A.CallTo(() => appEntity.Name)
.Returns(name); .Returns(name);

5
frontend/app/features/apps/pages/apps-page.component.ts

@ -7,6 +7,7 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { AppDto, AppsState, AuthService, DialogModel, FeatureDto, LocalStoreService, NewsService, OnboardingService, UIOptions, UIState } from '@app/shared'; import { AppDto, AppsState, AuthService, DialogModel, FeatureDto, LocalStoreService, NewsService, OnboardingService, UIOptions, UIState } from '@app/shared';
import { Settings } from '@app/shared/state/settings';
import { take } from 'rxjs/operators'; import { take } from 'rxjs/operators';
@Component({ @Component({
@ -48,7 +49,7 @@ export class AppsPageComponent implements OnInit {
this.onboardingService.disable('dialog'); this.onboardingService.disable('dialog');
this.onboardingDialog.show(); this.onboardingDialog.show();
} else if (!this.uiOptions.get('hideNews')) { } else if (!this.uiOptions.get('hideNews')) {
const newsVersion = this.localStore.getInt('squidex.news.version'); const newsVersion = this.localStore.getInt(Settings.Local.NEWS_VERSION);
this.newsService.getFeatures(newsVersion) this.newsService.getFeatures(newsVersion)
.subscribe(result => { .subscribe(result => {
@ -58,7 +59,7 @@ export class AppsPageComponent implements OnInit {
this.newsDialog.show(); this.newsDialog.show();
} }
this.localStore.setInt('squidex.news.version', result.version); this.localStore.setInt(Settings.Local.NEWS_VERSION, result.version);
} }
}); });
} }

5
frontend/app/features/assets/pages/assets-page.component.ts

@ -7,6 +7,7 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { AssetsState, DialogModel, LocalStoreService, Queries, Query, ResourceOwner, Router2State, UIState } from '@app/shared'; import { AssetsState, DialogModel, LocalStoreService, Queries, Query, ResourceOwner, Router2State, UIState } from '@app/shared';
import { Settings } from '@app/shared/state/settings';
@Component({ @Component({
selector: 'sqx-assets-page', selector: 'sqx-assets-page',
@ -31,7 +32,7 @@ export class AssetsPageComponent extends ResourceOwner implements OnInit {
) { ) {
super(); super();
this.isListView = this.localStore.getBoolean('squidex.assets.list-view'); this.isListView = this.localStore.getBoolean(Settings.Local.ASSETS_MODE);
} }
public ngOnInit() { public ngOnInit() {
@ -57,6 +58,6 @@ export class AssetsPageComponent extends ResourceOwner implements OnInit {
public changeView(isListView: boolean) { public changeView(isListView: boolean) {
this.isListView = isListView; this.isListView = isListView;
this.localStore.setBoolean('squidex.assets.list-view', isListView); this.localStore.setBoolean(Settings.Local.ASSETS_MODE, isListView);
} }
} }

4
frontend/app/features/content/pages/content/content-field.component.ts

@ -6,7 +6,7 @@
*/ */
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { AppLanguageDto, AppsState, EditContentForm, FieldForm, invalid$, LocalStoreService, SchemaDto, StringFieldPropertiesDto, TranslationsService, Types, value$ } from '@app/shared'; import { AppLanguageDto, AppsState, EditContentForm, FieldForm, invalid$, LocalStoreService, SchemaDto, Settings, StringFieldPropertiesDto, TranslationsService, Types, value$ } from '@app/shared';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { combineLatest } from 'rxjs/operators'; import { combineLatest } from 'rxjs/operators';
@ -157,6 +157,6 @@ export class ContentFieldComponent implements OnChanges {
} }
private configKey() { private configKey() {
return `squidex.schemas.${this.schema?.id}.fields.${this.formModel.field.fieldId}.show-all`; return Settings.Local.FIELD_ALL(this.schema?.id, this.formModel.field.fieldId);
} }
} }

4
frontend/app/features/content/pages/content/content-section.component.ts

@ -6,7 +6,7 @@
*/ */
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core'; import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core';
import { AppLanguageDto, EditContentForm, FieldForm, FieldSection, LocalStoreService, RootFieldDto, SchemaDto } from '@app/shared'; import { AppLanguageDto, EditContentForm, FieldForm, FieldSection, LocalStoreService, RootFieldDto, SchemaDto, Settings } from '@app/shared';
@Component({ @Component({
selector: 'sqx-content-section', selector: 'sqx-content-section',
@ -65,6 +65,6 @@ export class ContentSectionComponent implements OnChanges {
} }
private configKey(): string { private configKey(): string {
return `squidex.schemas.${this.schema?.id}.fields.${this.formSection?.separator?.fieldId}.closed`; return Settings.Local.FIELD_COLLAPSED(this.schema?.id, this.formSection?.separator?.fieldId);
} }
} }

6
frontend/app/features/content/pages/schemas/schemas-page.component.ts

@ -7,7 +7,7 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms'; import { FormControl } from '@angular/forms';
import { LocalStoreService, SchemaCategory, SchemasState } from '@app/shared'; import { LocalStoreService, SchemaCategory, SchemasState, Settings } from '@app/shared';
@Component({ @Component({
selector: 'sqx-schemas-page', selector: 'sqx-schemas-page',
@ -27,7 +27,7 @@ export class SchemasPageComponent implements OnInit {
public readonly schemasState: SchemasState, public readonly schemasState: SchemasState,
private readonly localStore: LocalStoreService private readonly localStore: LocalStoreService
) { ) {
this.isCollapsed = localStore.getBoolean('content.schemas.collapsed'); this.isCollapsed = localStore.getBoolean(Settings.Local.SCHEMAS_COLLAPSED);
} }
public ngOnInit() { public ngOnInit() {
@ -37,7 +37,7 @@ export class SchemasPageComponent implements OnInit {
public toggle() { public toggle() {
this.isCollapsed = !this.isCollapsed; this.isCollapsed = !this.isCollapsed;
this.localStore.setBoolean('content.schemas.collapsed', this.isCollapsed); this.localStore.setBoolean(Settings.Local.SCHEMAS_COLLAPSED, this.isCollapsed);
} }
public trackByCategory(_index: number, category: SchemaCategory) { public trackByCategory(_index: number, category: SchemaCategory) {

4
frontend/app/features/content/shared/forms/assets-editor.component.ts

@ -8,7 +8,7 @@
import { CdkDragDrop } from '@angular/cdk/drag-drop'; import { CdkDragDrop } from '@angular/cdk/drag-drop';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, OnInit } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { AppsState, AssetDto, AssetsService, DialogModel, LocalStoreService, MessageBus, sorted, StatefulControlComponent, Types } from '@app/shared'; import { AppsState, AssetDto, AssetsService, DialogModel, LocalStoreService, MessageBus, Settings, sorted, StatefulControlComponent, Types } from '@app/shared';
export const SQX_ASSETS_EDITOR_CONTROL_VALUE_ACCESSOR: any = { export const SQX_ASSETS_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => AssetsEditorComponent), multi: true provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => AssetsEditorComponent), multi: true
@ -59,7 +59,7 @@ export class AssetsEditorComponent extends StatefulControlComponent<State, Reado
super(changeDetector, { super(changeDetector, {
assets: [], assets: [],
assetFiles: [], assetFiles: [],
isListView: localStore.getBoolean('squidex.assets.list-view') isListView: localStore.getBoolean(Settings.Local.ASSETS_MODE)
}); });
} }

4
frontend/app/features/content/shared/preview-button.component.ts

@ -6,7 +6,7 @@
*/ */
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
import { ContentDto, fadeAnimation, interpolate, LocalStoreService, ModalModel, SchemaDetailsDto, StatefulComponent } from '@app/shared'; import { ContentDto, fadeAnimation, interpolate, LocalStoreService, ModalModel, SchemaDetailsDto, Settings, StatefulComponent } from '@app/shared';
interface State { interface State {
// The name of the selected preview config. // The name of the selected preview config.
@ -88,6 +88,6 @@ export class PreviewButtonComponent extends StatefulComponent<State> implements
} }
private configKey() { private configKey() {
return `squidex.schemas.${this.schema.id}.preview-button`; return Settings.Local.SCHEMA_PREVIEW(this.schema.id);
} }
} }

6
frontend/app/features/dashboard/pages/dashboard-page.component.ts

@ -8,7 +8,7 @@
// tslint:disable: readonly-array // tslint:disable: readonly-array
import { AfterViewInit, Component, NgZone, OnInit, Renderer2, ViewChild } from '@angular/core'; import { AfterViewInit, Component, NgZone, OnInit, Renderer2, ViewChild } from '@angular/core';
import { AppsState, AuthService, CallsUsageDto, CurrentStorageDto, DateTime, fadeAnimation, LocalStoreService, ResourceOwner, StorageUsagePerDateDto, UsagesService } from '@app/shared'; import { AppsState, AuthService, CallsUsageDto, CurrentStorageDto, DateTime, fadeAnimation, LocalStoreService, ResourceOwner, Settings, StorageUsagePerDateDto, UsagesService } from '@app/shared';
import { GridsterComponent, GridsterConfig, GridsterItem, GridType } from 'angular-gridster2'; import { GridsterComponent, GridsterConfig, GridsterItem, GridType } from 'angular-gridster2';
import { switchMap } from 'rxjs/operators'; import { switchMap } from 'rxjs/operators';
@ -46,7 +46,7 @@ export class DashboardPageComponent extends ResourceOwner implements AfterViewIn
) { ) {
super(); super();
this.isStacked = localStore.getBoolean('dashboard.charts.stacked'); this.isStacked = localStore.getBoolean(Settings.Local.DASHBOARD_CHART_STACKED);
} }
public ngOnInit() { public ngOnInit() {
@ -92,7 +92,7 @@ export class DashboardPageComponent extends ResourceOwner implements AfterViewIn
} }
public changeIsStacked(value: boolean) { public changeIsStacked(value: boolean) {
this.localStore.setBoolean('dashboard.charts.stacked', value); this.localStore.setBoolean(Settings.Local.DASHBOARD_CHART_STACKED, value);
this.isStacked = value; this.isStacked = value;
} }

2
frontend/app/features/settings/pages/languages/language.component.ts

@ -72,8 +72,6 @@ export class LanguageComponent implements OnChanges {
this.languagesState.update(this.language, request) this.languagesState.update(this.language, request)
.subscribe(() => { .subscribe(() => {
this.editForm.submitCompleted({ noReset: true }); this.editForm.submitCompleted({ noReset: true });
this.toggleEditing();
}, error => { }, error => {
this.editForm.submitFailed(error); this.editForm.submitFailed(error);
}); });

5
frontend/app/features/settings/pages/languages/languages-page.component.html

@ -17,7 +17,10 @@
<sqx-list-view [isLoading]="languagesState.isLoading | async"> <sqx-list-view [isLoading]="languagesState.isLoading | async">
<div content> <div content>
<ng-container *ngIf="(languagesState.isLoaded | async) && (languagesState.languages | async); let languages"> <ng-container *ngIf="(languagesState.isLoaded | async) && (languagesState.languages | async); let languages">
<sqx-language *ngFor="let languageInfo of languages; trackBy: trackByLanguage" [language]="languageInfo.language" [fallbackLanguages]="languageInfo.fallbackLanguages" [fallbackLanguagesNew]="languageInfo.fallbackLanguagesNew"> <sqx-language *ngFor="let languageInfo of languages; trackBy: trackByLanguage"
[language]="languageInfo.language"
[fallbackLanguages]="languageInfo.fallbackLanguages"
[fallbackLanguagesNew]="languageInfo.fallbackLanguagesNew">
</sqx-language> </sqx-language>
<sqx-language-add-form *ngIf="languagesState.canCreate | async" [newLanguages]="languagesState.newLanguages | async"> <sqx-language-add-form *ngIf="languagesState.canCreate | async" [newLanguages]="languagesState.newLanguages | async">

84
frontend/app/features/settings/pages/roles/role.component.html

@ -37,19 +37,27 @@
</div> </div>
<div class="table-items-row-details-tab"> <div class="table-items-row-details-tab">
<h3>{{ 'roles.permissions' | sqxTranslate }}</h3>
<sqx-form-hint marginBottom="3">
{{ 'roles.permissionsDescription' | sqxTranslate }}
</sqx-form-hint>
<ng-container *ngIf="!role.isDefaultRole; else defaultRole"> <ng-container *ngIf="!role.isDefaultRole; else defaultRole">
<div class="form-group row no-gutters" *ngFor="let control of editForm.controls; let i = index"> <table class="table table-bordered table-fixed table-sm">
<div class="col"> <tr *ngFor="let control of editForm.controls; let i = index">
<sqx-control-errors [for]="control" [fieldName]="'Permission'"></sqx-control-errors> <td class="col-name">
<sqx-control-errors [for]="control" [fieldName]="'Permission'"></sqx-control-errors>
<sqx-autocomplete [underlined]="true" [formControl]="control" [source]="allPermissions"></sqx-autocomplete> <sqx-autocomplete inputStyle="empty" [formControl]="control" [source]="allPermissions"></sqx-autocomplete>
</div> </td>
<div class="col-auto pl-1" *ngIf="isEditable"> <td class="col-action" *ngIf="isEditable">
<button type="button" class="btn btn-text-danger" (click)="removePermission(i)"> <button type="button" class="btn btn-text-danger" (click)="removePermission(i)">
<i class="icon-bin2"></i> <i class="icon-bin2"></i>
</button> </button>
</div> </td>
</div> </tr>
</table>
</ng-container> </ng-container>
<ng-template #defaultRole> <ng-template #defaultRole>
@ -57,21 +65,65 @@
{{descriptions[role.name] | sqxTranslate}} {{descriptions[role.name] | sqxTranslate}}
</sqx-form-alert> </sqx-form-alert>
<div class="form-group" *ngFor="let control of editForm.controls"> <table class="table table-bordered table-fixed table-sm">
<input class="form-control form-underlined" [formControl]="control"> <tr *ngFor="let control of editForm.controls; let i = index">
</div> <td>
<input class="form-control form-control-empty" [formControl]="control" />
</td>
</tr>
</table>
</ng-template> </ng-template>
<form class="form-group row no-gutters" [formGroup]="addPermissionForm.form" (ngSubmit)="addPermission()" *ngIf="isEditable"> <form class="form-group row no-gutters" [formGroup]="addPermissionForm.form" (ngSubmit)="addPermission()" *ngIf="isEditable">
<div class="col"> <div class="col">
<sqx-autocomplete [autoFocus]="true" formControlName="permission" [source]="allPermissions" #addInput></sqx-autocomplete> <sqx-autocomplete formControlName="permission" [source]="allPermissions" #addInput
placeholder="{{ 'i18n:roles.permissionsPlaceholder' | sqxTranslate }}">
</sqx-autocomplete>
</div> </div>
<div class="col-auto pl-1"> <div class="col-auto pl-1">
<button type="submit" class="btn btn-success"> <button type="submit" class="btn btn-success col-action">
<i class="icon-plus"></i> <i class="icon-plus"></i>
</button> </button>
</div> </div>
</form> </form>
<div class="mt-4" *ngIf="!role.isDefaultRole">
<h3>{{ 'roles.properties' | sqxTranslate }}</h3>
<sqx-form-hint marginBottom="3">
{{ 'roles.propertiesDescription' | sqxTranslate }}
</sqx-form-hint>
<div class="row rule-section">
<div class="form-check col-6" *ngFor="let property of propertiesSimple; trackBy: trackByProperty">
<input class="form-check-input" type="checkbox" id="{{role.name}}_{{property.key}}"
[disabled]="!isEditable"
[ngModel]="getProperty(property.key)"
(ngModelChange)="setProperty(property.key, $event)"
[ngModelOptions]="standalone">
<label class="form-check-label" for="{{role.name}}{{property.key}}">
{{ property.name | sqxTranslate }}
</label>
</div>
</div>
<div class="mt-4" *ngIf="schemas && schemas.length > 0">
<h5>{{ 'common.schemas' | sqxTranslate }}</h5>
<div class="row rule-section">
<div class="form-check col-6" *ngFor="let schema of schemas; trackBy: trackBySchema">
<input class="form-check-input" type="checkbox" id="{{role.name}}_schema_{{schema.name}}"
[disabled]="!isEditable"
[ngModel]="getProperty(propertiesList.HIDE_CONTENTS(schema.name))"
(ngModelChange)="setProperty(propertiesList.HIDE_CONTENTS(schema.name), $event)"
[ngModelOptions]="standalone">
<label class="form-check-label truncate" for="{{role.name}}_schema_{{schema.name}}">
{{ 'roles.properties.hideContents' | sqxTranslate: { schema: schema.displayName } }}
</label>
</div>
</div>
</div>
</div>
</div> </div>
</form> </form>
</div> </div>

24
frontend/app/features/settings/pages/roles/role.component.scss

@ -16,10 +16,34 @@
padding-right: 0; padding-right: 0;
} }
.form-control-empty {
background: 0;
border: 0;
}
.rule-name { .rule-name {
@include truncate; @include truncate;
} }
.rule-section {
margin-left: 0;
margin-right: 0;
}
.col-name {
overflow: visible;
padding-left: 0;
padding-right: 0;
width: 100%;
}
.col-action {
overflow: visible;
padding-left: 0;
padding-right: 0;
width: 42px;
}
.text-force { .text-force {
color: $color-text; color: $color-text;
} }

55
frontend/app/features/settings/pages/roles/role.component.ts

@ -7,15 +7,31 @@
import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges, ViewChild } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges, ViewChild } from '@angular/core';
import { FormBuilder } from '@angular/forms'; import { FormBuilder } from '@angular/forms';
import { AddPermissionForm, AutocompleteComponent, AutocompleteSource, EditRoleForm, RoleDto, RolesState } from '@app/shared'; import { AddPermissionForm, AutocompleteComponent, AutocompleteSource, EditRoleForm, RoleDto, RolesState, SchemaDto, Settings } from '@app/shared';
const Descriptions = { const DESCRIPTIONS = {
Developer: 'i18n:roles.defaults.developer', Developer: 'i18n:roles.defaults.developer',
Editor: 'i18n:roles.defaults.editor', Editor: 'i18n:roles.defaults.editor',
Owner: 'i18n:roles.default.owner', Owner: 'i18n:roles.default.owner',
Reader: 'i18n:roles.default.reader' Reader: 'i18n:roles.default.reader'
}; };
type Property = { name: string, key: string };
const SIMPLE_PROPERTIES: ReadonlyArray<Property> = [{
name: 'roles.properties.hideSchemas',
key: Settings.AppProperties.HIDE_SCHEMAS
}, {
name: 'roles.properties.hideAssets',
key: Settings.AppProperties.HIDE_ASSETS
}, {
name: 'roles.properties.hideSettings',
key: Settings.AppProperties.HIDE_SETTINGS
}, {
name: 'roles.properties.hideAPI',
key: Settings.AppProperties.HIDE_API
}];
@Component({ @Component({
selector: 'sqx-role', selector: 'sqx-role',
styleUrls: ['./role.component.scss'], styleUrls: ['./role.component.scss'],
@ -23,18 +39,27 @@ const Descriptions = {
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class RoleComponent implements OnChanges { export class RoleComponent implements OnChanges {
public readonly standalone = { standalone: true };
@Input() @Input()
public role: RoleDto; public role: RoleDto;
@Input() @Input()
public allPermissions: AutocompleteSource; public allPermissions: AutocompleteSource;
@Input()
public schemas: ReadonlyArray<SchemaDto>;
@ViewChild('addInput', { static: false }) @ViewChild('addInput', { static: false })
public addPermissionInput: AutocompleteComponent; public addPermissionInput: AutocompleteComponent;
public descriptions = Descriptions; public descriptions = DESCRIPTIONS;
public isEditing = false; public propertiesList = Settings.AppProperties;
public properties: {};
public propertiesSimple = SIMPLE_PROPERTIES;
public isEditing = true;
public isEditable = false; public isEditable = false;
public addPermissionForm = new AddPermissionForm(this.formBuilder); public addPermissionForm = new AddPermissionForm(this.formBuilder);
@ -51,15 +76,25 @@ export class RoleComponent implements OnChanges {
if (changes['role']) { if (changes['role']) {
this.isEditable = this.role.canUpdate; this.isEditable = this.role.canUpdate;
this.properties = this.role.properties;
this.editForm.load(this.role); this.editForm.load(this.role);
this.editForm.setEnabled(this.isEditable); this.editForm.setEnabled(this.isEditable);
} }
} }
public getProperty(name: string) {
return this.properties[name];
}
public toggleEditing() { public toggleEditing() {
this.isEditing = !this.isEditing; this.isEditing = !this.isEditing;
} }
public setProperty(name: string, value: boolean) {
this.properties[name] = value;
}
public delete() { public delete() {
this.rolesState.delete(this.role); this.rolesState.delete(this.role);
} }
@ -83,14 +118,20 @@ export class RoleComponent implements OnChanges {
const value = this.editForm.submit(); const value = this.editForm.submit();
if (value) { if (value) {
this.rolesState.update(this.role, value) this.rolesState.update(this.role, { ...value, properties: this.properties })
.subscribe(() => { .subscribe(() => {
this.editForm.submitCompleted({ noReset: true }); this.editForm.submitCompleted({ noReset: true });
this.toggleEditing();
}, error => { }, error => {
this.editForm.submitFailed(error); this.editForm.submitFailed(error);
}); });
} }
} }
public trackByProperty(_index: number, property: Property) {
return property.key;
}
public trackBySchema(_index: number, schema: SchemaDto) {
return schema.id;
}
} }

4
frontend/app/features/settings/pages/roles/roles-page.component.html

@ -17,7 +17,9 @@
<sqx-list-view [isLoading]="rolesState.isLoading | async"> <sqx-list-view [isLoading]="rolesState.isLoading | async">
<div content> <div content>
<ng-container *ngIf="(rolesState.isLoaded | async) && (rolesState.roles | async); let roles"> <ng-container *ngIf="(rolesState.isLoaded | async) && (rolesState.roles | async); let roles">
<sqx-role *ngFor="let role of roles" [role]="role" [allPermissions]="allPermissions"></sqx-role> <sqx-role *ngFor="let role of roles; trackBy: trackByRole" [role]="role"
[schemas]="schemasState.schemas | async" [allPermissions]="allPermissions">
</sqx-role>
<sqx-role-add-form *ngIf="rolesState.canCreate | async"></sqx-role-add-form> <sqx-role-add-form *ngIf="rolesState.canCreate | async"></sqx-role-add-form>
</ng-container> </ng-container>

7
frontend/app/features/settings/pages/roles/roles-page.component.ts

@ -6,7 +6,7 @@
*/ */
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { AppsState, AutocompleteSource, RoleDto, RolesService, RolesState } from '@app/shared'; import { AppsState, AutocompleteSource, RoleDto, RolesService, RolesState, SchemasState } from '@app/shared';
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
class PermissionsAutocomplete implements AutocompleteSource { class PermissionsAutocomplete implements AutocompleteSource {
@ -32,11 +32,14 @@ export class RolesPageComponent implements OnInit {
constructor( constructor(
private readonly appsState: AppsState, private readonly appsState: AppsState,
public readonly rolesService: RolesService, public readonly rolesService: RolesService,
public readonly rolesState: RolesState public readonly rolesState: RolesState,
public readonly schemasState: SchemasState
) { ) {
} }
public ngOnInit() { public ngOnInit() {
this.schemasState.loadIfNotLoaded();
this.rolesState.load(); this.rolesState.load();
} }

3
frontend/app/framework/angular/forms/editors/autocomplete.component.html

@ -6,7 +6,8 @@
autocomplete="off" autocomplete="off"
autocorrect="off" autocorrect="off"
autocapitalize="off" autocapitalize="off"
[class.form-underlined]="underlined" [class.form-empty]="inputStyle === 'empty'"
[class.form-underlined]="inputStyle === 'underlined'"
[class.form-icon]="!!icon" [class.form-icon]="!!icon"
[formControl]="queryInput"> [formControl]="queryInput">

6
frontend/app/framework/angular/forms/editors/autocomplete.component.ts

@ -56,6 +56,9 @@ export class AutocompleteComponent extends StatefulControlComponent<State, Reado
@Input() @Input()
public inputName = 'autocompletion'; public inputName = 'autocompletion';
@Input()
public inputStyle: 'underlined' | 'empty';
@Input() @Input()
public displayProperty: string; public displayProperty: string;
@ -68,9 +71,6 @@ export class AutocompleteComponent extends StatefulControlComponent<State, Reado
@Input() @Input()
public autoFocus = false; public autoFocus = false;
@Input()
public underlined = false;
@Input() @Input()
public debounceTime = 300; public debounceTime = 300;

2
frontend/app/framework/angular/forms/editors/checkbox-group.component.scss

@ -5,6 +5,6 @@
} }
.form-check-input { .form-check-input {
margin-top: .4rem; margin-top: .3rem;
} }
} }

6
frontend/app/shared/components/assets/assets-selector.component.ts

@ -6,7 +6,7 @@
*/ */
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, OnInit, Output } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, OnInit, Output } from '@angular/core';
import { AssetDto, AssetsState, LocalStoreService, Query, StatefulComponent } from '@app/shared/internal'; import { AssetDto, AssetsState, LocalStoreService, Query, Settings, StatefulComponent } from '@app/shared/internal';
interface State { interface State {
// The selected assets. // The selected assets.
@ -36,7 +36,7 @@ export class AssetsSelectorComponent extends StatefulComponent<State> implements
super(changeDector, { super(changeDector, {
selectedAssets: {}, selectedAssets: {},
selectionCount: 0, selectionCount: 0,
isListView: localStore.getBoolean('squidex.assets.list-view') isListView: localStore.getBoolean(Settings.Local.ASSETS_MODE)
}); });
} }
@ -83,6 +83,6 @@ export class AssetsSelectorComponent extends StatefulComponent<State> implements
public changeView(isListView: boolean) { public changeView(isListView: boolean) {
this.next(s => ({ ...s, isListView })); this.next(s => ({ ...s, isListView }));
this.localStore.setBoolean('squidex.assets.list-view', isListView); this.localStore.setBoolean(Settings.Local.ASSETS_MODE, isListView);
} }
} }

6
frontend/app/shared/components/forms/geolocation-editor.component.ts

@ -7,7 +7,7 @@
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, ViewChild } from '@angular/core'; import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, ViewChild } from '@angular/core';
import { FormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms'; import { FormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms';
import { LocalStoreService, ResourceLoaderService, StatefulControlComponent, Types, UIOptions, ValidatorsEx } from '@app/shared/internal'; import { LocalStoreService, ResourceLoaderService, Settings, StatefulControlComponent, Types, UIOptions, ValidatorsEx } from '@app/shared/internal';
declare var L: any; declare var L: any;
declare var google: any; declare var google: any;
@ -76,7 +76,7 @@ export class GeolocationEditorComponent extends StatefulControlComponent<State,
private readonly formBuilder: FormBuilder, private readonly formBuilder: FormBuilder,
private readonly uiOptions: UIOptions private readonly uiOptions: UIOptions
) { ) {
super(changeDetector, { isMapHidden: localStore.getBoolean('hideMap') }); super(changeDetector, { isMapHidden: localStore.getBoolean(Settings.Local.HIDE_MAP) });
this.isGoogleMaps = uiOptions.get('map.type') !== 'OSM'; this.isGoogleMaps = uiOptions.get('map.type') !== 'OSM';
} }
@ -84,7 +84,7 @@ export class GeolocationEditorComponent extends StatefulControlComponent<State,
public hideMap(isMapHidden: boolean) { public hideMap(isMapHidden: boolean) {
this.next({ isMapHidden }); this.next({ isMapHidden });
this.localStore.setBoolean('hideMap', isMapHidden); this.localStore.setBoolean(Settings.Local.HIDE_MAP, isMapHidden);
} }
public writeValue(obj: any) { public writeValue(obj: any) {

6
frontend/app/shared/components/schema-category.component.ts

@ -8,6 +8,8 @@
import { CdkDragDrop } from '@angular/cdk/drag-drop'; import { CdkDragDrop } from '@angular/cdk/drag-drop';
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core'; import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core';
import { fadeAnimation, LocalStoreService, SchemaCategory, SchemaDto, SchemasList, SchemasState } from '@app/shared/internal'; import { fadeAnimation, LocalStoreService, SchemaCategory, SchemaDto, SchemasList, SchemasState } from '@app/shared/internal';
import { AppsState } from '../state/apps.state';
import { Settings } from '../state/settings';
@Component({ @Component({
selector: 'sqx-schema-category', selector: 'sqx-schema-category',
@ -36,6 +38,7 @@ export class SchemaCategoryComponent implements OnChanges {
public isCollapsed = false; public isCollapsed = false;
constructor( constructor(
private readonly appsState: AppsState,
private readonly localStore: LocalStoreService, private readonly localStore: LocalStoreService,
private readonly schemasState: SchemasState private readonly schemasState: SchemasState
) { ) {
@ -51,7 +54,10 @@ export class SchemaCategoryComponent implements OnChanges {
this.filteredSchemas = this.schemaCategory.schemas; this.filteredSchemas = this.schemaCategory.schemas;
if (this.forContent) { if (this.forContent) {
const app = this.appsState.snapshot.selectedApp!;
this.filteredSchemas = this.filteredSchemas.filter(x => x.canReadContents && x.isPublished); this.filteredSchemas = this.filteredSchemas.filter(x => x.canReadContents && x.isPublished);
this.filteredSchemas = this.filteredSchemas.filter(x => !app.roleProperties[Settings.AppProperties.HIDE_CONTENTS(x.name)]);
} }
if (this.schemasFilter) { if (this.schemasFilter) {

1
frontend/app/shared/internal.ts

@ -66,6 +66,7 @@ export * from './state/rules.state';
export * from './state/schema-tag-source'; export * from './state/schema-tag-source';
export * from './state/schemas.forms'; export * from './state/schemas.forms';
export * from './state/schemas.state'; export * from './state/schemas.state';
export * from './state/settings';
export * from './state/table-fields'; export * from './state/table-fields';
export * from './state/ui.state'; export * from './state/ui.state';
export * from './state/workflows.forms'; export * from './state/workflows.forms';

10
frontend/app/shared/services/apps.service.spec.ts

@ -227,6 +227,7 @@ describe('AppsService', () => {
canAccessContent: id % 2 === 0, canAccessContent: id % 2 === 0,
planName: 'Free', planName: 'Free',
planUpgrade: 'Basic', planUpgrade: 'Basic',
roleProperties: createProperties(id),
version: id, version: id,
_links: { _links: {
schemas: { method: 'GET', href: '/schemas' } schemas: { method: 'GET', href: '/schemas' }
@ -251,5 +252,14 @@ export function createApp(id: number, suffix = '') {
id % 2 === 0, id % 2 === 0,
id % 2 === 0, id % 2 === 0,
'Free', 'Basic', 'Free', 'Basic',
createProperties(id),
new Version(`${id}${suffix}`)); new Version(`${id}${suffix}`));
} }
function createProperties(id: number) {
const result = {};
result[`property${id}`] = true;
return result;
}

2
frontend/app/shared/services/apps.service.ts

@ -48,6 +48,7 @@ export class AppDto {
public readonly canAccessContent: boolean, public readonly canAccessContent: boolean,
public readonly planName: string | undefined, public readonly planName: string | undefined,
public readonly planUpgrade: string | undefined, public readonly planUpgrade: string | undefined,
public readonly roleProperties: {},
public readonly version: Version public readonly version: Version
) { ) {
this._links = links; this._links = links;
@ -220,5 +221,6 @@ function parseApp(response: any) {
response.canAccessContent, response.canAccessContent,
response.planName, response.planName,
response.planUpgrade, response.planUpgrade,
response.roleProperties,
new Version(response.version.toString())); new Version(response.version.toString()));
} }

26
frontend/app/shared/services/roles.service.spec.ts

@ -99,7 +99,7 @@ describe('RolesService', () => {
it('should make put request to update role', it('should make put request to update role',
inject([RolesService, HttpTestingController], (roleService: RolesService, httpMock: HttpTestingController) => { inject([RolesService, HttpTestingController], (roleService: RolesService, httpMock: HttpTestingController) => {
const dto = { permissions: ['P4', 'P5'] }; const dto = { permissions: ['P4', 'P5'], properties: createProperties(1) };
const resource: Resource = { const resource: Resource = {
_links: { _links: {
@ -162,7 +162,8 @@ describe('RolesService', () => {
name: `name${id}`, name: `name${id}`,
numClients: id * 2, numClients: id * 2,
numContributors: id * 3, numContributors: id * 3,
permissions: [`permission${id}`], permissions: createPermissions(id),
properties: createProperties(id),
isDefaultRole: id % 2 === 0, isDefaultRole: id % 2 === 0,
_links: { _links: {
update: { method: 'PUT', href: `/roles/id${id}` } update: { method: 'PUT', href: `/roles/id${id}` }
@ -190,5 +191,24 @@ export function createRole(id: number) {
update: { method: 'PUT', href: `/roles/id${id}` } update: { method: 'PUT', href: `/roles/id${id}` }
}; };
return new RoleDto(links, `name${id}`, id * 2, id * 3, [`permission${id}`], id % 2 === 0); return new RoleDto(links, `name${id}`, id * 2, id * 3,
createPermissions(id),
createProperties(id),
id % 2 === 0);
}
function createPermissions(id: number) {
const result: string[] = [];
result.push(`permission${id}`);
return result;
}
function createProperties(id: number) {
const result = {};
result[`property${id}`] = true;
return result;
} }

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

Loading…
Cancel
Save