diff --git a/backend/extensions/Squidex.Extensions/Actions/RuleHelper.cs b/backend/extensions/Squidex.Extensions/Actions/RuleHelper.cs index 1869638ea..c912bbb3f 100644 --- a/backend/extensions/Squidex.Extensions/Actions/RuleHelper.cs +++ b/backend/extensions/Squidex.Extensions/Actions/RuleHelper.cs @@ -18,7 +18,7 @@ namespace Squidex.Extensions.Actions { 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)) { diff --git a/backend/i18n/frontend_en.json b/backend/i18n/frontend_en.json index cc3761743..d923512e9 100644 --- a/backend/i18n/frontend_en.json +++ b/backend/i18n/frontend_en.json @@ -550,6 +550,16 @@ "roles.deleteConfirmTitle": "Do you really want to delete the role?", "roles.loadFailed": "Failed to load roles. Please reload.", "roles.loadPermissionsFailed": "Failed to load permissions. Please reload.", + "roles.permissions": "Permissions", + "roles.permissionsDescription": "Permissions restrict the allowed operations and queries at API level and are a security feature.", + "roles.permissionsPlaceholder": "Start typing to search for permissions", + "roles.properties": "Properties", + "roles.properties.hideAPI": "Hide API", + "roles.properties.hideAssets": "Hide Assets", + "roles.properties.hideContents": "Hide {schema} Contents", + "roles.properties.hideSchemas": "Hide Schemas", + "roles.properties.hideSettings": "Hide Settings", + "roles.propertiesDescription": "Properties describe the behavior of the Management UI, but do not provide security for the API.", "roles.refreshTooltip": "Refresh roles (CTRL + SHIFT + R)", "roles.reloaded": "Roles reloaded.", "roles.revokeFailed": "Failed to revoke role. Please reload.", diff --git a/backend/i18n/frontend_it.json b/backend/i18n/frontend_it.json index a27b7c9e1..0fcf9432b 100644 --- a/backend/i18n/frontend_it.json +++ b/backend/i18n/frontend_it.json @@ -550,6 +550,16 @@ "roles.deleteConfirmTitle": "Sei sicuro di voler eliminare il ruolo?", "roles.loadFailed": "Non è stato possibile caricare i ruoli. Per favore ricarica.", "roles.loadPermissionsFailed": "Non è stato possibile caricare i permessi. Per favore ricarica.", + "roles.permissions": "Permissions", + "roles.permissionsDescription": "Permissions restrict the allowed operations and queries at API level and are a security feature.", + "roles.permissionsPlaceholder": "Start typing to search for permissions", + "roles.properties": "Properties", + "roles.properties.hideAPI": "Hide API", + "roles.properties.hideAssets": "Hide Assets", + "roles.properties.hideContents": "Hide {schema} Contents", + "roles.properties.hideSchemas": "Hide Schemas", + "roles.properties.hideSettings": "Hide Settings", + "roles.propertiesDescription": "Properties describe the behavior of the Management UI, but do not provide security for the API.", "roles.refreshTooltip": "Aggiorna i ruoli (CTRL + SHIFT + R)", "roles.reloaded": "Ruoli ricaricati.", "roles.revokeFailed": "Non è stato possibile rimuovere il ruolo. Per favore ricarica.", diff --git a/backend/i18n/frontend_nl.json b/backend/i18n/frontend_nl.json index 616f42838..a537dbf5e 100644 --- a/backend/i18n/frontend_nl.json +++ b/backend/i18n/frontend_nl.json @@ -550,6 +550,16 @@ "roles.deleteConfirmTitle": "Wil je de rol echt verwijderen?", "roles.loadFailed": "Laden van rollen is mislukt. Laad opnieuw.", "roles.loadPermissionsFailed": "Kan machtigingen niet laden. Laad opnieuw.", + "roles.permissions": "Permissions", + "roles.permissionsDescription": "Permissions restrict the allowed operations and queries at API level and are a security feature.", + "roles.permissionsPlaceholder": "Start typing to search for permissions", + "roles.properties": "Properties", + "roles.properties.hideAPI": "Hide API", + "roles.properties.hideAssets": "Hide Assets", + "roles.properties.hideContents": "Hide {schema} Contents", + "roles.properties.hideSchemas": "Hide Schemas", + "roles.properties.hideSettings": "Hide Settings", + "roles.propertiesDescription": "Properties describe the behavior of the Management UI, but do not provide security for the API.", "roles.refreshTooltip": "Ververs rollen (CTRL + SHIFT + R)", "roles.reloaded": "Rollen opnieuw geladen.", "roles.revokeFailed": "Kan rol niet intrekken. Laad opnieuw.", diff --git a/backend/i18n/source/frontend_en.json b/backend/i18n/source/frontend_en.json index cc3761743..d923512e9 100644 --- a/backend/i18n/source/frontend_en.json +++ b/backend/i18n/source/frontend_en.json @@ -550,6 +550,16 @@ "roles.deleteConfirmTitle": "Do you really want to delete the role?", "roles.loadFailed": "Failed to load roles. Please reload.", "roles.loadPermissionsFailed": "Failed to load permissions. Please reload.", + "roles.permissions": "Permissions", + "roles.permissionsDescription": "Permissions restrict the allowed operations and queries at API level and are a security feature.", + "roles.permissionsPlaceholder": "Start typing to search for permissions", + "roles.properties": "Properties", + "roles.properties.hideAPI": "Hide API", + "roles.properties.hideAssets": "Hide Assets", + "roles.properties.hideContents": "Hide {schema} Contents", + "roles.properties.hideSchemas": "Hide Schemas", + "roles.properties.hideSettings": "Hide Settings", + "roles.propertiesDescription": "Properties describe the behavior of the Management UI, but do not provide security for the API.", "roles.refreshTooltip": "Refresh roles (CTRL + SHIFT + R)", "roles.reloaded": "Roles reloaded.", "roles.revokeFailed": "Failed to revoke role. Please reload.", diff --git a/backend/src/Migrations/OldEvents/SchemaCreated.cs b/backend/src/Migrations/OldEvents/SchemaCreated.cs index 9f939aed9..95a73fbc2 100644 --- a/backend/src/Migrations/OldEvents/SchemaCreated.cs +++ b/backend/src/Migrations/OldEvents/SchemaCreated.cs @@ -52,7 +52,7 @@ namespace Migrations.OldEvents var field = eventField.Properties.CreateRootField(totalFields, eventField.Name, partitioning); - if (field is ArrayField arrayField && eventField.Nested?.Count > 0) + if (field is ArrayField arrayField && eventField.Nested?.Length > 0) { foreach (var nestedEventField in eventField.Nested) { diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs index 0d26b08c7..be2a1899a 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs @@ -36,16 +36,21 @@ namespace Squidex.Domain.Apps.Core.Apps Role = role; ApiCallsLimit = apiCallsLimit; - ApiTrafficLimit = apiTrafficLimit; AllowAnonymous = allowAnonymous; } [Pure] - public AppClient Update(string? name, string? role, long? apiCallsLimit, long? apiTrafficLimit, bool? allowAnonymous) + public AppClient Update(string? name, string? role, + long? apiCallsLimit, + long? apiTrafficLimit, + bool? allowAnonymous) { - return new AppClient(name.Or(Name), Secret, role.Or(Role), apiCallsLimit ?? ApiCallsLimit, apiTrafficLimit ?? ApiTrafficLimit, allowAnonymous ?? AllowAnonymous); + return new AppClient(name.Or(Name), Secret, role.Or(Role), + apiCallsLimit ?? ApiCallsLimit, + apiTrafficLimit ?? ApiTrafficLimit, + allowAnonymous ?? AllowAnonymous); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs index 77c32f985..e1f3f82b9 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs @@ -53,11 +53,16 @@ namespace Squidex.Domain.Apps.Core.Apps throw new ArgumentException("Id already exists.", nameof(id)); } - return Add(id, new AppClient(id, secret, Role.Editor)); + var newClient = new AppClient(id, secret, Role.Editor); + + return Add(id, newClient); } [Pure] - public AppClients Update(string id, string? name = null, string? role = null, long? apiCallsLimit = null, long? apiTrafficLimit = null, bool? allowAnonymous = false) + public AppClients Update(string id, string? name = null, string? role = null, + long? apiCallsLimit = null, + long? apiTrafficLimit = null, + bool? allowAnonymous = false) { Guard.NotNullOrEmpty(id, nameof(id)); @@ -66,7 +71,9 @@ namespace Squidex.Domain.Apps.Core.Apps return this; } - return With(id, client.Update(name, role, apiCallsLimit, apiTrafficLimit, allowAnonymous)); + var newClient = client.Update(name, role, apiCallsLimit, apiTrafficLimit, allowAnonymous); + + return With(id, newClient); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs index 606765773..d2af7879d 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs @@ -47,7 +47,9 @@ namespace Squidex.Domain.Apps.Core.Apps return this; } - return With(id, appPattern.Update(name, pattern, message)); + var newPattern = appPattern.Update(name, pattern, message); + + return With(id, newPattern); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonRole.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonRole.cs new file mode 100644 index 000000000..2f3192f8d --- /dev/null +++ b/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; } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/RoleConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/RoleConverter.cs new file mode 100644 index 000000000..b428ac6b0 --- /dev/null +++ b/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 + { + 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(); + var properties = (JsonObject?)null; + + if (reader.TokenType == JsonToken.StartArray) + { + permissions = serializer.Deserialize(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(reader)!; + break; + case "properties": + properties = serializer.Deserialize(reader)!; + break; + } + } + } + } + + return new JsonRole + { + Permissions = permissions, + Properties = properties ?? JsonValue.Object() + }; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/RolesConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/RolesConverter.cs index c8873a8e1..e7700d066 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/RolesConverter.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/RolesConverter.cs @@ -18,11 +18,15 @@ namespace Squidex.Domain.Apps.Core.Apps.Json { protected override void WriteValue(JsonWriter writer, Roles value, JsonSerializer serializer) { - var json = new Dictionary(value.CustomCount); + var json = new Dictionary(value.CustomCount); foreach (var role in value.Custom) { - json.Add(role.Name, role.Permissions.ToIds().ToArray()); + json.Add(role.Name, new JsonRole + { + Permissions = role.Permissions.ToIds().ToArray(), + Properties = role.Properties, + }); } serializer.Serialize(writer, json); @@ -30,14 +34,24 @@ namespace Squidex.Domain.Apps.Core.Apps.Json protected override Roles ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer) { - var json = serializer.Deserialize>(reader)!; + var json = serializer.Deserialize>(reader)!; if (json.Count == 0) { return Roles.Empty; } - return new Roles(json.ToDictionary(x => x.Key, x => new Role(x.Key, new PermissionSet(x.Value)))); + return new Roles(json.ToDictionary(x => x.Key, x => + { + var permissions = PermissionSet.Empty; + + if (x.Value.Permissions.Length > 0) + { + permissions = new PermissionSet(x.Value.Permissions); + } + + return new Role(x.Key, permissions, x.Value.Properties); + })); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs index fd1be52b9..9c99ac11e 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs @@ -7,9 +7,11 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Diagnostics.Contracts; using System.Linq; using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Security; using P = Squidex.Shared.Permissions; @@ -18,36 +20,68 @@ namespace Squidex.Domain.Apps.Core.Apps [Equals(DoNotAddEqualityOperators = true)] public sealed class Role : Named { + private static readonly HashSet ExtraPermissions = new HashSet + { + P.AppComments, + P.AppContributorsRead, + P.AppHistory, + P.AppHistory, + P.AppLanguagesRead, + P.AppPatternsRead, + P.AppPing, + P.AppRolesRead, + P.AppSchemasRead, + P.AppSearch, + P.AppTranslate, + P.AppUsage + }; + public const string Editor = "Editor"; public const string Developer = "Developer"; public const string Owner = "Owner"; public const string Reader = "Reader"; + public static readonly ReadOnlyCollection EmptyProperties = new ReadOnlyCollection(new List()); + public PermissionSet Permissions { get; } + public JsonObject Properties { get; } + [IgnoreDuringEquals] public bool IsDefault { get { return Roles.IsDefault(this); } } - public Role(string name, PermissionSet permissions) + public Role(string name, PermissionSet permissions, JsonObject properties) : base(name) { Guard.NotNull(permissions, nameof(permissions)); + Guard.NotNull(properties, nameof(properties)); Permissions = permissions; + Properties = properties; + } + + public static Role WithPermissions(string role, params string[] permissions) + { + return new Role(role, new PermissionSet(permissions), JsonValue.Object()); + } + + public static Role WithProperties(string role, JsonObject properties) + { + return new Role(role, PermissionSet.Empty, properties); } - public Role(string name, params string[] permissions) - : this(name, new PermissionSet(permissions)) + public static Role Create(string role) { + return new Role(role, PermissionSet.Empty, JsonValue.Object()); } [Pure] - public Role Update(string[] permissions) + public Role Update(PermissionSet? permissions, JsonObject? properties) { - return new Role(Name, new PermissionSet(permissions)); + return new Role(Name, permissions ?? Permissions, properties ?? Properties); } public bool Equals(string name) @@ -55,12 +89,11 @@ namespace Squidex.Domain.Apps.Core.Apps return name != null && name.Equals(Name, StringComparison.Ordinal); } - public Role ForApp(string app) + public Role ForApp(string app, bool isFrontend = false) { - var result = new HashSet - { - P.ForApp(P.AppCommon, app) - }; + Guard.NotNullOrEmpty(app, nameof(app)); + + var result = new HashSet(); 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); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs index 5f9f010f1..0851e72b2 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs @@ -12,6 +12,7 @@ using System.Diagnostics.Contracts; using System.Linq; using Squidex.Infrastructure; using Squidex.Infrastructure.Collections; +using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Security; using Squidex.Shared; @@ -24,28 +25,37 @@ namespace Squidex.Domain.Apps.Core.Apps public static readonly IReadOnlyDictionary Defaults = new Dictionary { [Role.Owner] = - new Role(Role.Owner, new PermissionSet( - Clean(Permissions.App))), + new Role(Role.Owner, + new PermissionSet( + Clean(Permissions.App)), + JsonValue.Object()), [Role.Reader] = - new Role(Role.Reader, new PermissionSet( - Clean(Permissions.AppAssetsRead), - Clean(Permissions.AppContentsRead))), + new Role(Role.Reader, + new PermissionSet( + Clean(Permissions.AppAssetsRead), + Clean(Permissions.AppContentsRead)), + JsonValue.Object() + .Add("ui.api.hide", true)), [Role.Editor] = - new Role(Role.Editor, new PermissionSet( - Clean(Permissions.AppAssets), - Clean(Permissions.AppContents), - Clean(Permissions.AppRolesRead), - Clean(Permissions.AppWorkflowsRead))), + new Role(Role.Editor, + new PermissionSet( + Clean(Permissions.AppAssets), + Clean(Permissions.AppContents), + Clean(Permissions.AppRolesRead), + Clean(Permissions.AppWorkflowsRead)), + JsonValue.Object() + .Add("ui.api.hide", true)), [Role.Developer] = - new Role(Role.Developer, new PermissionSet( - Clean(Permissions.AppApi), - Clean(Permissions.AppAssets), - Clean(Permissions.AppContents), - Clean(Permissions.AppPatterns), - Clean(Permissions.AppRolesRead), - Clean(Permissions.AppRules), - Clean(Permissions.AppSchemas), - Clean(Permissions.AppWorkflows))) + new Role(Role.Developer, + new PermissionSet( + Clean(Permissions.AppAssets), + Clean(Permissions.AppContents), + Clean(Permissions.AppPatterns), + Clean(Permissions.AppRolesRead), + Clean(Permissions.AppRules), + Clean(Permissions.AppSchemas), + Clean(Permissions.AppWorkflows)), + JsonValue.Object()), }; public static readonly Roles Empty = new Roles(new ImmutableDictionary()); @@ -89,8 +99,6 @@ namespace Squidex.Domain.Apps.Core.Apps [Pure] public Roles Add(string name) { - var newRole = new Role(name); - if (inner.ContainsKey(name)) { return this; @@ -101,21 +109,24 @@ namespace Squidex.Domain.Apps.Core.Apps return this; } + var newRole = Role.Create(name); + return Create(inner.With(name, newRole)); } [Pure] - public Roles Update(string name, params string[] permissions) + public Roles Update(string name, PermissionSet? permissions = null, JsonObject? properties = null) { Guard.NotNullOrEmpty(name, nameof(name)); - Guard.NotNull(permissions, nameof(permissions)); if (!inner.TryGetValue(name, out var role)) { return this; } - return Create(inner.With(name, role.Update(permissions))); + var newRole = role.Update(permissions, properties); + + return Create(inner.With(name, newRole)); } public static bool IsDefault(string role) @@ -138,19 +149,22 @@ namespace Squidex.Domain.Apps.Core.Apps return inner.ContainsKey(name) || Defaults.ContainsKey(name); } - public bool TryGet(string app, string name, [MaybeNullWhen(false)] out Role value) + public bool TryGet(string app, string name, bool isFrontend, [MaybeNullWhen(false)] out Role value) { Guard.NotNull(app, nameof(app)); - if (Defaults.TryGetValue(name, out var role) || inner.TryGetValue(name, out role)) + value = null!; + + if (Defaults.TryGetValue(name, out var role)) { - value = role.ForApp(app); - return true; + value = role.ForApp(app, isFrontend && name != Role.Owner); + } + else if (inner.TryGetValue(name, out role)) + { + value = role.ForApp(app, isFrontend); } - value = null!; - - return false; + return value != null; } private static string Clean(string permission) diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs index bc9d138c4..f8ba0ef96 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs @@ -213,7 +213,7 @@ namespace Squidex.Domain.Apps.Core.EventSynchronization if (sourceNames.SetEquals(targetNames) && !sourceNames.SequenceEqual(targetNames)) { - var fieldIds = targetNames.Select(x => sourceIds.Find(y => y.Name == x)!.Id).ToList(); + var fieldIds = targetNames.Select(x => sourceIds.Find(y => y.Name == x)!.Id).ToArray(); yield return new SchemaFieldsReordered { FieldIds = fieldIds, ParentFieldId = parentId }; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppSettingsSearchSource.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppSettingsSearchSource.cs index 46a66ccd3..88aed5ae2 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppSettingsSearchSource.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppSettingsSearchSource.cs @@ -56,19 +56,16 @@ namespace Squidex.Domain.Apps.Entities.Apps Search("Clients", Permissions.AppClientsRead, urlGenerator.ClientsUI, SearchResultType.Setting); - Search("Contents", Permissions.AppCommon, - urlGenerator.ContentsUI, SearchResultType.Content); - Search("Contributors", Permissions.AppContributorsRead, urlGenerator.ContributorsUI, SearchResultType.Setting); - Search("Dashboard", Permissions.AppCommon, + Search("Dashboard", Permissions.AppUsage, urlGenerator.DashboardUI, SearchResultType.Dashboard); - Search("Languages", Permissions.AppCommon, + Search("Languages", Permissions.AppLanguagesRead, urlGenerator.LanguagesUI, SearchResultType.Setting); - Search("Patterns", Permissions.AppCommon, + Search("Patterns", Permissions.AppPatternsRead, urlGenerator.PatternsUI, SearchResultType.Setting); Search("Roles", Permissions.AppRolesRead, @@ -77,7 +74,7 @@ namespace Squidex.Domain.Apps.Entities.Apps Search("Rules", Permissions.AppRulesRead, urlGenerator.RulesUI, SearchResultType.Rule); - Search("Schemas", Permissions.AppCommon, + Search("Schemas", Permissions.AppSchemasRead, urlGenerator.SchemasUI, SearchResultType.Schema); Search("Subscription", Permissions.AppPlansRead, diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateLanguage.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateLanguage.cs index 07ee21cde..1ea40ee38 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateLanguage.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateLanguage.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Collections.Generic; using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities.Apps.Commands @@ -18,6 +17,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.Commands public bool IsMaster { get; set; } - public List? Fallback { get; set; } + public Language[]? Fallback { get; set; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateRole.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateRole.cs index 3d7c648f7..1386849a9 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateRole.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateRole.cs @@ -5,6 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using Squidex.Infrastructure.Json.Objects; + namespace Squidex.Domain.Apps.Entities.Apps.Commands { public sealed class UpdateRole : AppCommand @@ -12,5 +14,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Commands public string Name { get; set; } public string[] Permissions { get; set; } + + public JsonObject? Properties { get; set; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppLanguages.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppLanguages.cs index 90f5ea3a8..ae1041a8c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppLanguages.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppLanguages.cs @@ -81,7 +81,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards e(T.Get("apps.languages.masterLanguageNotOptional"), nameof(command.IsMaster)); } - if (command.Fallback?.Count > 0) + if (command.Fallback?.Length > 0) { e(T.Get("apps.languages.masterLanguageNoFallbacks"), nameof(command.Fallback)); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs index 0404b46c8..40a1a4e65 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs +++ b/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); case AppPlanChanged e when Is.Change(Plan?.PlanId, e.PlanId): - return UpdatePlan(e, ev => new AppPlan(ev.Actor, ev.PlanId)); + return UpdatePlan(e, ev => ev.ToAppPlan()); case AppPlanReset e when Plan != null: return UpdatePlan(e, ev => null); @@ -112,7 +112,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.State return UpdateRoles(e, (ev, r) => r.Add(ev.Name)); case AppRoleUpdated e: - return UpdateRoles(e, (ev, r) => r.Update(ev.Name, ev.Permissions)); + return UpdateRoles(e, (ev, r) => r.Update(ev.Name, ev.ToPermissions(), ev.Properties)); case AppRoleDeleted e: return UpdateRoles(e, (ev, r) => r.Remove(ev.Name)); @@ -126,7 +126,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.State case AppLanguageUpdated e: return UpdateLanguages(e, (ev, l) => { - l = l.Set(ev.Language, ev.IsOptional, ev.Fallback?.ToArray()); + l = l.Set(ev.Language, ev.IsOptional, ev.Fallback); if (ev.IsMaster) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/SchemaBuilder.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/SchemaBuilder.cs index a1bd59e64..65d05accb 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/SchemaBuilder.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/SchemaBuilder.cs @@ -6,7 +6,7 @@ // ========================================================================== using System; -using System.Collections.Generic; +using System.Linq; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Entities.Schemas.Commands; using Squidex.Text; @@ -144,8 +144,14 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates.Builders } }; - command.Fields ??= new List(); - command.Fields.Add(field); + if (command.Fields == null) + { + command.Fields = new[] { field }; + } + else + { + command.Fields = command.Fields.Union(Enumerable.Repeat(field, 1)).ToArray(); + } return field; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/BulkUpdateCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/BulkUpdateCommandMiddleware.cs index 35376007e..c78165ee5 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/BulkUpdateCommandMiddleware.cs +++ b/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 (bulkUpdates.Jobs?.Count > 0) + if (bulkUpdates.Jobs?.Length > 0) { var requestContext = contextProvider.Context.WithoutContentEnrichment().WithUnpublished(true); var requestedSchema = bulkUpdates.SchemaId.Name; - var results = new BulkUpdateResultItem[bulkUpdates.Jobs.Count]; + var results = new BulkUpdateResultItem[bulkUpdates.Jobs.Length]; var actionBlock = new ActionBlock(async index => { @@ -125,7 +125,7 @@ namespace Squidex.Domain.Apps.Entities.Contents MaxDegreeOfParallelism = Math.Max(1, Environment.ProcessorCount / 2) }); - for (var i = 0; i < bulkUpdates.Jobs.Count; i++) + for (var i = 0; i < bulkUpdates.Jobs.Length; i++) { await actionBlock.SendAsync(i); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/BulkUpdateContents.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/BulkUpdateContents.cs index 6ebcfecdd..3ceaa3495 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/BulkUpdateContents.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/BulkUpdateContents.cs @@ -6,7 +6,6 @@ // ========================================================================== using System; -using System.Collections.Generic; using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities.Contents.Commands @@ -25,6 +24,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Commands public bool OptimizeValidation { get; set; } - public List? Jobs { get; set; } + public BulkUpdateJob[]? Jobs { get; set; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureFieldRules.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureFieldRules.cs index a20df4232..3218dc0d1 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureFieldRules.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureFieldRules.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Collections.Generic; using System.Linq; using Squidex.Domain.Apps.Core.Schemas; @@ -13,11 +12,11 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Commands { public sealed class ConfigureFieldRules : SchemaCommand { - public List? FieldRules { get; set; } + public FieldRuleCommand[]? FieldRules { get; set; } public FieldRules ToFieldRules() { - if (FieldRules?.Count > 0) + if (FieldRules?.Length > 0) { return new FieldRules(FieldRules.Select(x => x.ToFieldRule()).ToList()); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ReorderFields.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ReorderFields.cs index 227443431..3409a2cb0 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ReorderFields.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ReorderFields.cs @@ -5,12 +5,10 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Collections.Generic; - namespace Squidex.Domain.Apps.Entities.Schemas.Commands { public sealed class ReorderFields : ParentFieldCommand { - public List FieldIds { get; set; } + public long[] FieldIds { get; set; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertCommand.cs index 00489c0d5..a88ff24e1 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertCommand.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertCommand.cs @@ -9,8 +9,7 @@ using System.Collections.Generic; using System.Linq; using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core.Schemas; -using FieldRules = System.Collections.Generic.List; -using SchemaFields = System.Collections.Generic.List; +using SchemaField = Squidex.Domain.Apps.Entities.Schemas.Commands.UpsertSchemaField; namespace Squidex.Domain.Apps.Entities.Schemas.Commands { @@ -20,13 +19,13 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Commands public string Category { get; set; } - public SchemaFields Fields { get; set; } + public SchemaField[]? Fields { get; set; } public FieldNames? FieldsInReferences { get; set; } public FieldNames? FieldsInLists { get; set; } - public FieldRules? FieldRules { get; set; } + public FieldRuleCommand[]? FieldRules { get; set; } public SchemaScripts? Scripts { get; set; } @@ -85,7 +84,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Commands var field = eventField.Properties.CreateRootField(totalFields, eventField.Name, partitioning); - if (field is ArrayField arrayField && eventField.Nested?.Count > 0) + if (field is ArrayField arrayField && eventField.Nested?.Length > 0) { foreach (var nestedEventField in eventField.Nested) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaField.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaField.cs index 0625e2220..df58306f4 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaField.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaField.cs @@ -5,14 +5,12 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Collections.Generic; - namespace Squidex.Domain.Apps.Entities.Schemas.Commands { public sealed class UpsertSchemaField : UpsertSchemaFieldBase { public string Partitioning { get; set; } - public List Nested { get; set; } + public UpsertSchemaNestedField[]? Nested { get; set; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs index d4d8c27cd..8bd98ad34 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs +++ b/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) { - if (command.Fields?.Count > 0) + if (command.Fields?.Length > 0) { command.Fields.Foreach((field, fieldIndex) => { @@ -182,7 +182,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards ValidateField(field, prefix, e); - if (field.Nested?.Count > 0) + if (field.Nested?.Length > 0) { if (field.Properties is ArrayFieldProperties) { @@ -193,7 +193,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards ValidateNestedField(nestedField, nestedPrefix, e); }); } - else if (field.Nested.Count > 0) + else if (field.Nested.Length > 0) { e(T.Get("schemas.onlyArraysHaveNested"), $"{prefix}.{nameof(field.Partitioning)}"); } @@ -292,7 +292,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards } } - private static void ValidateFieldRules(List? fieldRules, string path, AddValidation e) + private static void ValidateFieldRules(FieldRuleCommand[]? fieldRules, string path, AddValidation e) { fieldRules?.Foreach((rule, ruleIndex) => { @@ -318,7 +318,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards { var fieldPrefix = $"{path}[{fieldIndex}]"; - var field = command?.Fields?.Find(x => x.Name == fieldName); + var field = command?.Fields?.FirstOrDefault(x => x.Name == fieldName); if (string.IsNullOrWhiteSpace(fieldName)) { @@ -356,7 +356,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards private static void ValidateFieldIds(ReorderFields c, IReadOnlyDictionary 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)); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs index 65879c991..bf25c492e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs @@ -6,6 +6,7 @@ // ========================================================================== using System; +using System.Linq; using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Events.Schemas; @@ -131,7 +132,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.State case SchemaFieldsReordered e: { - SchemaDef = SchemaDef.ReorderFields(e.FieldIds, e.ParentFieldId?.Id); + SchemaDef = SchemaDef.ReorderFields(e.FieldIds.ToList(), e.ParentFieldId?.Id); break; } diff --git a/backend/src/Squidex.Domain.Apps.Events/Apps/AppLanguageUpdated.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppLanguageUpdated.cs index a36e31055..39d50285a 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Apps/AppLanguageUpdated.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Apps/AppLanguageUpdated.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Collections.Generic; using Squidex.Infrastructure; using Squidex.Infrastructure.Reflection; @@ -20,6 +19,6 @@ namespace Squidex.Domain.Apps.Events.Apps public bool IsMaster { get; set; } - public List? Fallback { get; set; } + public Language[]? Fallback { get; set; } } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Apps/AppPlanChanged.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppPlanChanged.cs index f0ac9fdc8..e0354a233 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Apps/AppPlanChanged.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Apps/AppPlanChanged.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using Squidex.Domain.Apps.Core.Apps; using Squidex.Infrastructure.EventSourcing; namespace Squidex.Domain.Apps.Events.Apps @@ -13,5 +14,10 @@ namespace Squidex.Domain.Apps.Events.Apps public sealed class AppPlanChanged : AppEvent { public string PlanId { get; set; } + + public AppPlan ToAppPlan() + { + return new AppPlan(Actor, PlanId); + } } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Apps/AppRoleUpdated.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppRoleUpdated.cs index a826e0584..0d16c45a8 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Apps/AppRoleUpdated.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Apps/AppRoleUpdated.cs @@ -6,6 +6,8 @@ // ========================================================================== using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Json.Objects; +using Squidex.Infrastructure.Security; namespace Squidex.Domain.Apps.Events.Apps { @@ -15,5 +17,12 @@ namespace Squidex.Domain.Apps.Events.Apps public string Name { get; set; } public string[] Permissions { get; set; } + + public JsonObject? Properties { get; set; } + + public PermissionSet ToPermissions() + { + return new PermissionSet(Permissions); + } } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedField.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedField.cs index 1cf2fc2a8..118e374af 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedField.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedField.cs @@ -5,14 +5,12 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Collections.Generic; - namespace Squidex.Domain.Apps.Events.Schemas { public sealed class SchemaCreatedField : SchemaCreatedFieldBase { public string Partitioning { get; set; } - public List Nested { get; set; } + public SchemaCreatedNestedField[] Nested { get; set; } } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaFieldsReordered.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaFieldsReordered.cs index 1c1f4f7c4..b2ba66b98 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaFieldsReordered.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaFieldsReordered.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Collections.Generic; using Squidex.Infrastructure.EventSourcing; namespace Squidex.Domain.Apps.Events.Schemas @@ -13,6 +12,6 @@ namespace Squidex.Domain.Apps.Events.Schemas [EventType(nameof(SchemaFieldsReordered))] public sealed class SchemaFieldsReordered : ParentFieldEvent { - public List FieldIds { get; set; } + public long[] FieldIds { get; set; } } } diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/Grains/BatchSubscriber.cs b/backend/src/Squidex.Infrastructure/EventSourcing/Grains/BatchSubscriber.cs index 7cf94a5fb..0104dd160 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/Grains/BatchSubscriber.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/Grains/BatchSubscriber.cs @@ -22,7 +22,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains private readonly IEventSubscription eventSubscription; private readonly IDataflowBlock pipelineEnd; - public object Sender + public object? Sender { get { return eventSubscription.Sender; } } @@ -86,21 +86,21 @@ namespace Squidex.Infrastructure.EventSourcing.Grains var handle = new ActionBlock>(async jobs => { + var sender = eventSubscription.Sender; + foreach (var jobsBySender in jobs.GroupBy(x => x.Sender)) { - var sender = jobsBySender.Key; - - if (ReferenceEquals(sender, eventSubscription.Sender)) + if (sender != null && ReferenceEquals(jobsBySender.Key, sender)) { var exception = jobs.FirstOrDefault(x => x.Exception != null)?.Exception; if (exception != null) { - await grain.OnErrorAsync(Sender, exception); + await grain.OnErrorAsync(sender, exception); } else { - await grain.OnEventsAsync(Sender, GetEvents(jobsBySender), GetPosition(jobsBySender)); + await grain.OnEventsAsync(sender, GetEvents(jobsBySender), GetPosition(jobsBySender)); } } } diff --git a/backend/src/Squidex.Shared/Permissions.cs b/backend/src/Squidex.Shared/Permissions.cs index 71ddc30e1..433be0a27 100644 --- a/backend/src/Squidex.Shared/Permissions.cs +++ b/backend/src/Squidex.Shared/Permissions.cs @@ -50,13 +50,28 @@ namespace Squidex.Shared public const string AdminUsersLock = "squidex.admin.users.lock"; public const string App = "squidex.apps.{app}"; - public const string AppCommon = "squidex.apps.{app}.common"; public const string AppDelete = "squidex.apps.{app}.delete"; public const string AppUpdate = "squidex.apps.{app}.update"; public const string AppUpdateImage = "squidex.apps.{app}.update"; public const string AppUpdateGeneral = "squidex.apps.{app}.general"; + public const string AppHistory = "squidex.apps.{app}.history"; + + public const string AppPing = "squidex.apps.{app}.ping"; + + public const string AppSearch = "squidex.apps.{app}.search"; + + public const string AppTranslate = "squidex.apps.{app}.translate"; + + public const string AppUsage = "squidex.apps.{app}.usage"; + + public const string AppComments = "squidex.apps.{app}.comments"; + public const string AppCommentsRead = "squidex.apps.{app}.comments.read"; + public const string AppCommentsCreate = "squidex.apps.{app}.comments.create"; + public const string AppCommentsUpdate = "squidex.apps.{app}.comments.update"; + public const string AppCommentsDelete = "squidex.apps.{app}.comments.delete"; + public const string AppClients = "squidex.apps.{app}.clients"; public const string AppClientsRead = "squidex.apps.{app}.clients.read"; public const string AppClientsCreate = "squidex.apps.{app}.clients.create"; @@ -69,6 +84,7 @@ namespace Squidex.Shared public const string AppContributorsRevoke = "squidex.apps.{app}.contributors.revoke"; public const string AppLanguages = "squidex.apps.{app}.languages"; + public const string AppLanguagesRead = "squidex.apps.{app}.languages.read"; public const string AppLanguagesCreate = "squidex.apps.{app}.languages.create"; public const string AppLanguagesUpdate = "squidex.apps.{app}.languages.update"; public const string AppLanguagesDelete = "squidex.apps.{app}.languages.delete"; @@ -80,6 +96,7 @@ namespace Squidex.Shared public const string AppRolesDelete = "squidex.apps.{app}.roles.delete"; public const string AppPatterns = "squidex.apps.{app}.patterns"; + public const string AppPatternsRead = "squidex.apps.{app}.patterns.read"; public const string AppPatternsCreate = "squidex.apps.{app}.patterns.create"; public const string AppPatternsUpdate = "squidex.apps.{app}.patterns.update"; public const string AppPatternsDelete = "squidex.apps.{app}.patterns.delete"; @@ -115,6 +132,7 @@ namespace Squidex.Shared public const string AppRulesDelete = "squidex.apps.{app}.rules.delete"; public const string AppSchemas = "squidex.apps.{app}.schemas"; + public const string AppSchemasRead = "squidex.apps.{app}.schemas.read"; public const string AppSchemasCreate = "squidex.apps.{app}.schemas.create"; public const string AppSchemasUpdate = "squidex.apps.{app}.schemas.{name}.update"; public const string AppSchemasScripts = "squidex.apps.{app}.schemas.{name}.scripts"; @@ -130,8 +148,6 @@ namespace Squidex.Shared public const string AppContentsVersionDelete = "squidex.apps.{app}.contents.{name}.version.delete"; public const string AppContentsDelete = "squidex.apps.{app}.contents.{name}.delete"; - public const string AppApi = "squidex.apps.{app}.api"; - static Permissions() { foreach (var field in typeof(Permissions).GetFields(BindingFlags.Public | BindingFlags.Static)) diff --git a/backend/src/Squidex.Web/Pipeline/AppResolver.cs b/backend/src/Squidex.Web/Pipeline/AppResolver.cs index 54c8a92ff..42023a9c6 100644 --- a/backend/src/Squidex.Web/Pipeline/AppResolver.cs +++ b/backend/src/Squidex.Web/Pipeline/AppResolver.cs @@ -6,6 +6,7 @@ // ========================================================================== using System; +using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; @@ -41,9 +42,9 @@ namespace Squidex.Web.Pipeline if (!string.IsNullOrWhiteSpace(appName)) { - var canCache = !user.IsInClient(DefaultClients.Frontend); + var isFrontend = user.IsInClient(DefaultClients.Frontend); - var app = await appProvider.GetAppAsync(appName, canCache); + var app = await appProvider.GetAppAsync(appName, !isFrontend); if (app == null) { @@ -51,16 +52,16 @@ namespace Squidex.Web.Pipeline return; } - var (role, permissions) = FindByOpenIdSubject(app, user); + var (role, permissions) = FindByOpenIdSubject(app, user, isFrontend); if (permissions == null) { - (role, permissions) = FindByOpenIdClient(app, user); + (role, permissions) = FindByOpenIdClient(app, user, isFrontend); } if (permissions == null) { - (role, permissions) = FindAnonymousClient(app); + (role, permissions) = FindAnonymousClient(app, isFrontend); } if (permissions != null) @@ -132,7 +133,7 @@ namespace Squidex.Web.Pipeline return context.ActionDescriptor.EndpointMetadata.Any(x => x is AllowAnonymousAttribute); } - private static (string?, PermissionSet?) FindByOpenIdClient(IAppEntity app, ClaimsPrincipal user) + private static (string?, PermissionSet?) FindByOpenIdClient(IAppEntity app, ClaimsPrincipal user, bool isFrontend) { var (appName, clientId) = user.GetClient(); @@ -141,7 +142,7 @@ namespace Squidex.Web.Pipeline return (null, null); } - if (clientId != null && app.Clients.TryGetValue(clientId, out var client) && app.Roles.TryGet(app.Name, client.Role, out var role)) + if (clientId != null && app.Clients.TryGetValue(clientId, out var client) && app.Roles.TryGet(app.Name, client.Role, isFrontend, out var role)) { return (client.Role, role.Permissions); } @@ -149,11 +150,11 @@ namespace Squidex.Web.Pipeline return (null, null); } - private static (string?, PermissionSet?) FindAnonymousClient(IAppEntity app) + private static (string?, PermissionSet?) FindAnonymousClient(IAppEntity app, bool isFrontend) { var client = app.Clients.Values.FirstOrDefault(x => x.AllowAnonymous); - if (client != null && app.Roles.TryGet(app.Name, client.Role, out var role)) + if (client != null && app.Roles.TryGet(app.Name, client.Role, isFrontend, out var role)) { return (client.Role, role.Permissions); } @@ -161,11 +162,11 @@ namespace Squidex.Web.Pipeline return (null, null); } - private static (string?, PermissionSet?) FindByOpenIdSubject(IAppEntity app, ClaimsPrincipal user) + private static (string?, PermissionSet?) FindByOpenIdSubject(IAppEntity app, ClaimsPrincipal user, bool isFrontend) { var subjectId = user.OpenIdSubject(); - if (subjectId != null && app.Contributors.TryGetValue(subjectId, out var roleName) && app.Roles.TryGet(app.Name, roleName, out var role)) + if (subjectId != null && app.Contributors.TryGetValue(subjectId, out var roleName) && app.Roles.TryGet(app.Name, roleName, isFrontend, out var role)) { return (roleName, role.Permissions); } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs index 18a330b68..337085a18 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs @@ -48,7 +48,7 @@ namespace Squidex.Areas.Api.Controllers.Apps [HttpGet] [Route("apps/{app}/contributors/")] [ProducesResponseType(typeof(ContributorsDto), 200)] - [ApiPermissionOrAnonymous(Permissions.AppCommon)] + [ApiPermissionOrAnonymous(Permissions.AppContributorsRead)] [ApiCosts(0)] public IActionResult GetContributors(string app) { diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs index bc8daa29b..982473b98 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs @@ -41,7 +41,7 @@ namespace Squidex.Areas.Api.Controllers.Apps [HttpGet] [Route("apps/{app}/languages/")] [ProducesResponseType(typeof(AppLanguagesDto), 200)] - [ApiPermissionOrAnonymous(Permissions.AppCommon)] + [ApiPermissionOrAnonymous(Permissions.AppLanguagesRead)] [ApiCosts(0)] public IActionResult GetLanguages(string app) { diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs index 94ee344d3..8b7cc510e 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs @@ -43,7 +43,7 @@ namespace Squidex.Areas.Api.Controllers.Apps [HttpGet] [Route("apps/{app}/patterns/")] [ProducesResponseType(typeof(PatternsDto), 200)] - [ApiPermissionOrAnonymous(Permissions.AppCommon)] + [ApiPermissionOrAnonymous(Permissions.AppPatternsRead)] [ApiCosts(0)] public IActionResult GetPatterns(string app) { diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs index 536e8a576..2ae96ef8f 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs @@ -80,7 +80,9 @@ namespace Squidex.Areas.Api.Controllers.Apps var response = Deferred.Response(() => { - return apps.OrderBy(x => x.Name).Select(a => AppDto.FromApp(a, userOrClientId, appPlansProvider, Resources)).ToArray(); + var isFrontend = HttpContext.User.IsInClient(DefaultClients.Frontend); + + return apps.OrderBy(x => x.Name).Select(a => AppDto.FromApp(a, userOrClientId, isFrontend, appPlansProvider, Resources)).ToArray(); }); Response.Headers[HeaderNames.ETag] = apps.ToEtag(); @@ -108,7 +110,9 @@ namespace Squidex.Areas.Api.Controllers.Apps var userOrClientId = HttpContext.User.UserOrClientId()!; 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(); @@ -299,8 +303,10 @@ namespace Squidex.Areas.Api.Controllers.Apps var userOrClientId = HttpContext.User.UserOrClientId()!; + var isFrontend = HttpContext.User.IsInClient(DefaultClients.Frontend); + var result = context.Result(); - var response = AppDto.FromApp(result, userOrClientId, appPlansProvider, Resources); + var response = AppDto.FromApp(result, userOrClientId, isFrontend, appPlansProvider, Resources); return response; } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs index ab3a6719c..2a6563ce6 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs +++ b/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.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps.Plans; +using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Validation; @@ -73,6 +74,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models /// /// Indicates if the user can access the api. /// + [Obsolete("Usage role properties")] public bool CanAccessApi { get; set; } /// @@ -90,17 +92,30 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models /// public string? PlanUpgrade { get; set; } - public static AppDto FromApp(IAppEntity app, string userId, IAppPlansProvider plans, Resources resources) + /// + /// The properties from the role. + /// + [LocalizedRequired] + public JsonObject RoleProperties { get; set; } + + public static AppDto FromApp(IAppEntity app, string userId, bool isFrontend, IAppPlansProvider plans, Resources resources) { - var permissions = GetPermissions(app, userId); + var permissions = GetPermissions(app, userId, isFrontend); var result = SimpleMapper.Map(app, new AppDto()); result.Permissions = permissions.ToIds(); - if (resources.Includes(P.ForApp(P.AppApi, app.Name), permissions)) + result.SetPlan(app, plans, resources, permissions); + result.SetImage(app, resources); + + if (app.Contributors.TryGetValue(userId, out var roleName) && app.Roles.TryGet(app.Name, roleName, isFrontend, out var role)) + { + result.RoleProperties = role.Properties; + } + else { - result.CanAccessApi = true; + result.RoleProperties = JsonValue.Object(); } if (resources.Includes(P.ForApp(P.AppContents, app.Name), permissions)) @@ -108,17 +123,14 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models result.CanAccessContent = true; } - result.SetPlan(app, plans, resources, permissions); - result.SetImage(app, resources); - return result.CreateLinks(resources, permissions); } - private static PermissionSet GetPermissions(IAppEntity app, string userId) + private static PermissionSet GetPermissions(IAppEntity app, string userId, bool isFrontend) { var permissions = new List(); - if (app.Contributors.TryGetValue(userId, out var roleName) && app.Roles.TryGet(app.Name, roleName, out var role)) + if (app.Contributors.TryGetValue(userId, out var roleName) && app.Roles.TryGet(app.Name, roleName, isFrontend, out var role)) { permissions.AddRange(role.Permissions); } @@ -187,12 +199,12 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models AddGetLink("contributors", resources.Url(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(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(x => nameof(x.GetPatterns), values)); } @@ -212,7 +224,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models AddGetLink("rules", resources.Url(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(x => nameof(x.GetSchemas), values)); } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/RoleDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/RoleDto.cs index 7953cbac0..3038d81d0 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/RoleDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/RoleDto.cs @@ -5,10 +5,10 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Collections.Generic; using System.Linq; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Validation; using Squidex.Web; @@ -41,18 +41,23 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models /// Associated list of permissions. /// [LocalizedRequired] - public IEnumerable Permissions { get; set; } + public string[] Permissions { get; set; } + + /// + /// Associated list of UI properties. + /// + [LocalizedRequired] + public JsonObject Properties { get; set; } public static RoleDto FromRole(Role role, IAppEntity app) { - var permissions = role.Permissions; - var result = new RoleDto { Name = role.Name, NumClients = GetNumClients(role, app), NumContributors = GetNumContributors(role, app), - Permissions = permissions.ToIds(), + Permissions = role.Permissions.ToIds().ToArray(), + Properties = role.Properties, IsDefaultRole = role.IsDefault }; diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateLanguageDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateLanguageDto.cs index b23b48e0e..a5f8c5dd2 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateLanguageDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateLanguageDto.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Collections.Generic; using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Infrastructure; using Squidex.Infrastructure.Reflection; @@ -27,7 +26,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models /// /// Optional fallback languages. /// - public List? Fallback { get; set; } + public Language[]? Fallback { get; set; } public UpdateLanguage ToCommand(Language language) { diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateRoleDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateRoleDto.cs index bcc356964..7a0ad9354 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateRoleDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateRoleDto.cs @@ -6,6 +6,7 @@ // ========================================================================== using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Validation; namespace Squidex.Areas.Api.Controllers.Apps.Models @@ -18,9 +19,14 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models [LocalizedRequired] public string[] Permissions { get; set; } + /// + /// Associated list of UI properties. + /// + public JsonObject? Properties { get; set; } + public UpdateRole ToCommand(string name) { - return new UpdateRole { Name = name, Permissions = Permissions }; + return new UpdateRole { Name = name, Permissions = Permissions, Properties = Properties }; } } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateWorkflowDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateWorkflowDto.cs index 5c2fde490..2b04831d5 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateWorkflowDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateWorkflowDto.cs @@ -30,7 +30,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models /// /// The schema ids. /// - public List? SchemaIds { get; set; } + public IReadOnlyList? SchemaIds { get; set; } /// /// The initial step. diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetContentQueryDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetContentQueryDto.cs index 5beeb561e..b507a59c9 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetContentQueryDto.cs +++ b/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; } /// - /// True to force a new resize even if it already stored. + /// The target image format. /// [FromQuery(Name = "format")] public ImageFormat Format { get; set; } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs index b64d9e867..841e9f9ae 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs @@ -49,7 +49,7 @@ namespace Squidex.Areas.Api.Controllers.Comments [HttpGet] [Route("apps/{app}/comments/{commentsId}")] [ProducesResponseType(typeof(CommentsDto), 200)] - [ApiPermissionOrAnonymous(Permissions.AppCommon)] + [ApiPermissionOrAnonymous(Permissions.AppCommentsRead)] [ApiCosts(0)] public async Task GetComments(string app, string commentsId, [FromQuery] long version = EtagVersion.Any) { @@ -79,7 +79,7 @@ namespace Squidex.Areas.Api.Controllers.Comments [HttpPost] [Route("apps/{app}/comments/{commentsId}")] [ProducesResponseType(typeof(CommentDto), 201)] - [ApiPermissionOrAnonymous(Permissions.AppCommon)] + [ApiPermissionOrAnonymous(Permissions.AppCommentsCreate)] [ApiCosts(0)] public async Task PostComment(string app, string commentsId, [FromBody] UpsertCommentDto request) { @@ -106,7 +106,7 @@ namespace Squidex.Areas.Api.Controllers.Comments /// [HttpPut] [Route("apps/{app}/comments/{commentsId}/{commentId}")] - [ApiPermissionOrAnonymous(Permissions.AppCommon)] + [ApiPermissionOrAnonymous(Permissions.AppCommentsUpdate)] [ApiCosts(0)] public async Task PutComment(string app, string commentsId, Guid commentId, [FromBody] UpsertCommentDto request) { @@ -127,7 +127,7 @@ namespace Squidex.Areas.Api.Controllers.Comments /// [HttpDelete] [Route("apps/{app}/comments/{commentsId}/{commentId}")] - [ApiPermissionOrAnonymous(Permissions.AppCommon)] + [ApiPermissionOrAnonymous(Permissions.AppCommentsDelete)] [ApiCosts(0)] public async Task DeleteComment(string app, string commentsId, Guid commentId) { diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkUpdateDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkUpdateDto.cs index bee7918e9..8e920634a 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkUpdateDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkUpdateDto.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Collections.Generic; using System.Linq; using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Infrastructure.Reflection; @@ -19,7 +18,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models /// The contents to update or insert. /// [LocalizedRequired] - public List Jobs { get; set; } + public BulkUpdateJobDto[]? Jobs { get; set; } /// /// True to automatically publish the content. @@ -40,7 +39,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models { var result = SimpleMapper.Map(this, new BulkUpdateContents()); - result.Jobs = Jobs?.Select(x => x.ToJob())?.ToList(); + result.Jobs = Jobs?.Select(x => x.ToJob())?.ToArray(); return result; } diff --git a/backend/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs b/backend/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs index a12a3f265..9f7b3e16a 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs @@ -42,7 +42,7 @@ namespace Squidex.Areas.Api.Controllers.History [HttpGet] [Route("apps/{app}/history/")] [ProducesResponseType(typeof(HistoryEventDto), 200)] - [ApiPermissionOrAnonymous(Permissions.AppCommon)] + [ApiPermissionOrAnonymous(Permissions.AppHistory)] [ApiCosts(0.1)] public async Task GetHistory(string app, string channel) { diff --git a/backend/src/Squidex/Areas/Api/Controllers/Ping/PingController.cs b/backend/src/Squidex/Areas/Api/Controllers/Ping/PingController.cs index fa83c3818..1e1881131 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Ping/PingController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Ping/PingController.cs @@ -68,7 +68,7 @@ namespace Squidex.Areas.Api.Controllers.Ping /// [HttpGet] [Route("ping/{app}/")] - [ApiPermissionOrAnonymous(Permissions.AppCommon)] + [ApiPermissionOrAnonymous(Permissions.AppPing)] [ApiCosts(0)] public IActionResult GetAppPing(string app) { diff --git a/backend/src/Squidex/Areas/Api/Controllers/QueryDto.cs b/backend/src/Squidex/Areas/Api/Controllers/QueryDto.cs index 7272eddf1..2d5a693bf 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/QueryDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/QueryDto.cs @@ -6,7 +6,6 @@ // ========================================================================== using System; -using System.Collections.Generic; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Squidex.Domain.Apps.Entities; @@ -18,7 +17,7 @@ namespace Squidex.Areas.Api.Controllers /// /// The optional list of ids to query. /// - public List? Ids { get; set; } + public Guid[]? Ids { get; set; } /// /// The optional odata query. diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/ConfigureFieldRulesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/ConfigureFieldRulesDto.cs index 3619877f7..239b78218 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/ConfigureFieldRulesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/ConfigureFieldRulesDto.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Collections.Generic; using System.Linq; using Squidex.Domain.Apps.Entities.Schemas.Commands; @@ -16,13 +15,13 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models /// /// The field rules to configure. /// - public List? FieldRules { get; set; } + public FieldRuleDto[]? FieldRules { get; set; } public ConfigureFieldRules ToCommand() { return new ConfigureFieldRules { - FieldRules = FieldRules?.Select(x => x.ToCommand()).ToList() + FieldRules = FieldRules?.Select(x => x.ToCommand()).ToArray() }; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/ReorderFieldsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/ReorderFieldsDto.cs index 04b592fee..1e0b416e0 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/ReorderFieldsDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/ReorderFieldsDto.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Collections.Generic; using Squidex.Domain.Apps.Entities.Schemas.Commands; using Squidex.Infrastructure.Validation; @@ -17,7 +16,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models /// The field ids in the target order. /// [LocalizedRequired] - public List FieldIds { get; set; } + public long[] FieldIds { get; set; } public ReorderFields ToCommand(long? parentId = null) { diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaDto.cs index eb6fe9400..57dea96b4 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaDto.cs @@ -27,17 +27,17 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models /// /// The names of the fields that should be used in references. /// - public List? FieldsInReferences { get; set; } + public string[]? FieldsInReferences { get; set; } /// /// The names of the fields that should be shown in lists, including meta fields. /// - public List? FieldsInLists { get; set; } + public string[]? FieldsInLists { get; set; } /// /// Optional fields. /// - public List? Fields { get; set; } + public UpsertSchemaFieldDto[]? Fields { get; set; } /// /// The optional preview urls. @@ -82,9 +82,9 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models command.FieldsInReferences = new FieldNames(dto.FieldsInReferences); } - if (dto.Fields != null) + if (dto.Fields?.Length > 0) { - command.Fields = new List(); + var fields = new List(); foreach (var rootFieldDto in dto.Fields) { @@ -95,9 +95,9 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models { SimpleMapper.Map(rootFieldDto, rootField); - if (rootFieldDto?.Nested?.Count > 0) + if (rootFieldDto?.Nested?.Length > 0) { - rootField.Nested = new List(); + var nestedFields = new List(); foreach (var nestedFieldDto in rootFieldDto.Nested) { @@ -109,13 +109,17 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models SimpleMapper.Map(nestedFieldDto, nestedField); } - rootField.Nested.Add(nestedField); + nestedFields.Add(nestedField); } + + rootField.Nested = nestedFields.ToArray(); } } - command.Fields.Add(rootField); + fields.Add(rootField); } + + command.Fields = fields.ToArray(); } return command; diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaFieldDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaFieldDto.cs index fae6abe03..b608b9566 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaFieldDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaFieldDto.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Collections.Generic; using Squidex.Infrastructure.Validation; namespace Squidex.Areas.Api.Controllers.Schemas.Models @@ -48,6 +47,6 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models /// /// The nested fields. /// - public List? Nested { get; set; } + public UpsertSchemaNestedFieldDto[]? Nested { get; set; } } } \ No newline at end of file diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs index 640d087b0..6a0e2e91b 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs @@ -44,7 +44,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas [HttpGet] [Route("apps/{app}/schemas/")] [ProducesResponseType(typeof(SchemasDto), 200)] - [ApiPermissionOrAnonymous(Permissions.AppCommon)] + [ApiPermissionOrAnonymous(Permissions.AppSchemasRead)] [ApiCosts(0)] public async Task GetSchemas(string app) { @@ -72,7 +72,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas [HttpGet] [Route("apps/{app}/schemas/{name}/")] [ProducesResponseType(typeof(SchemaDetailsDto), 200)] - [ApiPermissionOrAnonymous(Permissions.AppCommon)] + [ApiPermissionOrAnonymous(Permissions.AppSchemasRead)] [ApiCosts(0)] public async Task GetSchema(string app, string name) { diff --git a/backend/src/Squidex/Areas/Api/Controllers/Search/SearchController.cs b/backend/src/Squidex/Areas/Api/Controllers/Search/SearchController.cs index c4351a411..ac9230937 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Search/SearchController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Search/SearchController.cs @@ -42,9 +42,9 @@ namespace Squidex.Areas.Api.Controllers.Search [HttpGet] [Route("apps/{app}/search/")] [ProducesResponseType(typeof(SearchResultDto[]), 200)] - [ApiPermissionOrAnonymous(Permissions.AppCommon)] + [ApiPermissionOrAnonymous(Permissions.AppSearch)] [ApiCosts(0)] - public async Task GetSchemas(string app, [FromQuery] string? query = null) + public async Task GetSearchResults(string app, [FromQuery] string? query = null) { var result = await searchManager.SearchAsync(query, Context); diff --git a/backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs b/backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs index 827862d34..34a41e5b2 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs @@ -66,7 +66,7 @@ namespace Squidex.Areas.Api.Controllers.Statistics [HttpGet] [Route("apps/{app}/usages/log/")] [ProducesResponseType(typeof(LogDownloadDto), 200)] - [ApiPermissionOrAnonymous(Permissions.AppCommon)] + [ApiPermissionOrAnonymous(Permissions.AppUsage)] [ApiCosts(0)] public IActionResult GetLog(string app) { @@ -93,7 +93,7 @@ namespace Squidex.Areas.Api.Controllers.Statistics [HttpGet] [Route("apps/{app}/usages/calls/{fromDate}/{toDate}/")] [ProducesResponseType(typeof(CallsUsageDtoDto), 200)] - [ApiPermissionOrAnonymous(Permissions.AppCommon)] + [ApiPermissionOrAnonymous(Permissions.AppUsage)] [ApiCosts(0)] public async Task GetUsages(string app, DateTime fromDate, DateTime toDate) { @@ -122,7 +122,7 @@ namespace Squidex.Areas.Api.Controllers.Statistics [HttpGet] [Route("apps/{app}/usages/storage/today/")] [ProducesResponseType(typeof(CurrentStorageDto), 200)] - [ApiPermissionOrAnonymous(Permissions.AppCommon)] + [ApiPermissionOrAnonymous(Permissions.AppUsage)] [ApiCosts(0)] public async Task GetCurrentStorageSize(string app) { @@ -149,7 +149,7 @@ namespace Squidex.Areas.Api.Controllers.Statistics [HttpGet] [Route("apps/{app}/usages/storage/{fromDate}/{toDate}/")] [ProducesResponseType(typeof(StorageUsagePerDateDto[]), 200)] - [ApiPermissionOrAnonymous(Permissions.AppCommon)] + [ApiPermissionOrAnonymous(Permissions.AppUsage)] [ApiCosts(0)] public async Task GetStorageSizes(string app, DateTime fromDate, DateTime toDate) { diff --git a/backend/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs index 6aa691b33..b4016457e 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs @@ -40,9 +40,9 @@ namespace Squidex.Areas.Api.Controllers.Translations [HttpPost] [Route("apps/{app}/translations/")] [ProducesResponseType(typeof(TranslationDto), 200)] - [ApiPermissionOrAnonymous(Permissions.AppCommon)] + [ApiPermissionOrAnonymous(Permissions.AppTranslate)] [ApiCosts(0)] - public async Task GetLanguages(string app, [FromBody] TranslateDto request) + public async Task PostTranslation(string app, [FromBody] TranslateDto request) { var result = await translator.Translate(request.Text, request.TargetLanguage, request.SourceLanguage, HttpContext.RequestAborted); var response = TranslationDto.FromTranslation(result); diff --git a/backend/src/Squidex/Config/Domain/SerializationServices.cs b/backend/src/Squidex/Config/Domain/SerializationServices.cs index 0543e944c..85620398d 100644 --- a/backend/src/Squidex/Config/Domain/SerializationServices.cs +++ b/backend/src/Squidex/Config/Domain/SerializationServices.cs @@ -47,6 +47,7 @@ namespace Squidex.Config.Domain new NamedStringIdConverter(), new PropertyPathConverter(), new RefTokenConverter(), + new RoleConverter(), new RolesConverter(), new RuleConverter(), new SchemaConverter(), diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RoleTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RoleTests.cs index 2d458821c..9d6f897e9 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RoleTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RoleTests.cs @@ -16,7 +16,7 @@ namespace Squidex.Domain.Apps.Core.Model.Apps [Fact] public void Should_be_default_role() { - var role = new Role("Owner"); + var role = Role.Create("Owner"); Assert.True(role.IsDefault); } @@ -24,25 +24,25 @@ namespace Squidex.Domain.Apps.Core.Model.Apps [Fact] public void Should_not_be_default_role() { - var role = new Role("Custom"); + var role = Role.Create("Custom"); Assert.False(role.IsDefault); } [Fact] - public void Should_add_common_permission() + public void Should_not_add_common_permission() { - var role = new Role("Name"); + var role = Role.Create("Name"); var result = role.ForApp("my-app").Permissions.ToIds(); - Assert.Equal(new[] { "squidex.apps.my-app.common" }, result); + Assert.Empty(result); } [Fact] public void Should_not_have_duplicate_permission() { - var role = new Role("Name", "common", "common", "common"); + var role = Role.WithPermissions("Name", "common", "common", "common"); var result = role.ForApp("my-app").Permissions.ToIds(); @@ -50,19 +50,19 @@ namespace Squidex.Domain.Apps.Core.Model.Apps } [Fact] - public void Should_ForApp_permission() + public void Should_append_app_prefix_to_permission() { - var role = new Role("Name", "clients.read"); + var role = Role.WithPermissions("Name", "clients.read"); var result = role.ForApp("my-app").Permissions.ToIds(); - Assert.Equal("squidex.apps.my-app.clients.read", result.ElementAt(1)); + Assert.Equal("squidex.apps.my-app.clients.read", result.ElementAt(0)); } [Fact] public void Should_check_for_name() { - var role = new Role("Custom"); + var role = Role.WithPermissions("Custom"); Assert.True(role.Equals("Custom")); } @@ -70,7 +70,7 @@ namespace Squidex.Domain.Apps.Core.Model.Apps [Fact] public void Should_check_for_null_name() { - var role = new Role("Custom"); + var role = Role.WithPermissions("Custom"); Assert.False(role.Equals((string)null!)); Assert.False(role.Equals("Other")); diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesJsonTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesJsonTests.cs index 12dd95b42..afd040de0 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesJsonTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesJsonTests.cs @@ -5,19 +5,56 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Collections.Generic; using FluentAssertions; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.TestHelpers; +using Squidex.Infrastructure.Json.Objects; +using Squidex.Infrastructure.Security; using Xunit; namespace Squidex.Domain.Apps.Core.Model.Apps { public class RolesJsonTests { + [Fact] + public void Should_deserialize_from_old_role_format() + { + var source = new Dictionary + { + ["Custom"] = new string[] + { + "Permission1", + "Permission2" + } + }; + + var expected = + Roles.Empty + .Add("Custom") + .Update("Custom", + new PermissionSet( + "Permission1", + "Permission2")); + + var roles = source.SerializeAndDeserialize(); + + roles.Should().BeEquivalentTo(expected); + } + [Fact] public void Should_serialize_and_deserialize() { - var sut = Roles.Empty.Add("Custom").Update("Custom", "Permission1", "Permission2"); + var sut = + Roles.Empty + .Add("Custom") + .Update("Custom", + new PermissionSet( + "Permission1", + "Permission2"), + JsonValue.Object() + .Add("Property1", true) + .Add("Property2", true)); var roles = sut.SerializeAndDeserialize(); diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs index 8a02262a8..6df89a16c 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs @@ -9,6 +9,7 @@ using System.Collections.Generic; using System.Linq; using FluentAssertions; using Squidex.Domain.Apps.Core.Apps; +using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Security; using Xunit; @@ -40,7 +41,7 @@ namespace Squidex.Domain.Apps.Core.Model.Apps { var roles_1 = roles_0.Add(role); - roles_1[role].Should().BeEquivalentTo(new Role(role, PermissionSet.Empty)); + roles_1[role].Should().BeEquivalentTo(Role.Create(role)); } [Fact] @@ -61,15 +62,23 @@ namespace Squidex.Domain.Apps.Core.Model.Apps } [Fact] - public void Should_update_role() + public void Should_update_role_permissions() { - var roles_1 = roles_0.Update(firstRole, "P1", "P2"); + var roles_1 = roles_0.Update(firstRole, permissions: new PermissionSet("P1", "P2")); - roles_1[firstRole].Should().BeEquivalentTo(new Role(firstRole, new PermissionSet("P1", "P2"))); + roles_1[firstRole].Should().BeEquivalentTo(Role.WithPermissions(firstRole, "P1", "P2")); } [Fact] - public void Should_return_same_roles_if_role_is_updated_with_same_values() + public void Should_update_role_properties() + { + var roles_1 = roles_0.Update(firstRole, properties: JsonValue.Object().Add("P1", true)); + + roles_1[firstRole].Should().BeEquivalentTo(Role.WithProperties(firstRole, JsonValue.Object().Add("P1", true))); + } + + [Fact] + public void Should_return_same_roles_if_role_is_updated_with_same_permissions() { var roles_1 = roles_0.Update(firstRole); @@ -79,7 +88,7 @@ namespace Squidex.Domain.Apps.Core.Model.Apps [Fact] public void Should_return_same_roles_if_role_not_found() { - var roles_1 = roles_0.Update(role, "P1", "P2"); + var roles_1 = roles_0.Update(role, permissions: new PermissionSet("P1", "P2")); Assert.Same(roles_0, roles_1); } @@ -142,14 +151,14 @@ namespace Squidex.Domain.Apps.Core.Model.Apps Assert.False(Roles.IsDefault(firstRole)); } - [InlineData("Developer")] - [InlineData("Editor")] - [InlineData("Owner")] - [InlineData("Reader")] + [InlineData("Developer", 7)] + [InlineData("Editor", 4)] + [InlineData("Reader", 2)] + [InlineData("Owner", 1)] [Theory] - public void Should_get_default_roles(string name) + public void Should_get_default_roles(string name, int permissionCount) { - var found = roles_0.TryGet("app", name, out var result); + var found = roles_0.TryGet("app", name, false, out var result); Assert.True(found); Assert.True(result!.IsDefault); @@ -158,13 +167,29 @@ namespace Squidex.Domain.Apps.Core.Model.Apps foreach (var permission in result.Permissions) { Assert.StartsWith("squidex.apps.app.", permission.Id); + Assert.DoesNotContain("{app}", permission.Id); } + + Assert.Equal(permissionCount, result!.Permissions.Count); + } + + [InlineData("Developer", 17)] + [InlineData("Editor", 14)] + [InlineData("Reader", 13)] + [InlineData("Owner", 1)] + [Theory] + public void Should_add_extra_permissions_for_frontend_client(string name, int permissionCount) + { + var found = roles_0.TryGet("app", name, true, out var result); + + Assert.True(found); + Assert.Equal(permissionCount, result!.Permissions.Count); } [Fact] public void Should_return_null_if_role_not_found() { - var found = roles_0.TryGet("app", "custom", out var result); + var found = roles_0.TryGet("app", "custom", false, out var result); Assert.False(found); Assert.Null(result); diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/SchemaSynchronizerTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/SchemaSynchronizerTests.cs index 3e57ade43..2ddaedda2 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/SchemaSynchronizerTests.cs +++ b/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); events.ShouldHaveSameEvents( - new SchemaFieldsReordered { FieldIds = new List { 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); events.ShouldHaveSameEvents( - new SchemaFieldsReordered { FieldIds = new List { 11, 10 } } + new SchemaFieldsReordered { FieldIds = new[] { 11L, 10L } } ); } @@ -620,7 +620,7 @@ namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization events.ShouldHaveSameEvents( new FieldDeleted { FieldId = NamedId.Of(11L, "f2") }, new FieldAdded { FieldId = NamedId.Of(50L, "f3"), Name = "f3", Partitioning = Partitioning.Invariant.Key, Properties = new StringFieldProperties() }, - new SchemaFieldsReordered { FieldIds = new List { 50, 10 } } + new SchemaFieldsReordered { FieldIds = new[] { 50L, 10L } } ); } @@ -642,7 +642,7 @@ namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization events.ShouldHaveSameEvents( new FieldAdded { FieldId = NamedId.Of(50L, "f3"), Name = "f3", Partitioning = Partitioning.Invariant.Key, Properties = new StringFieldProperties() }, - new SchemaFieldsReordered { FieldIds = new List { 10, 50, 11 } } + new SchemaFieldsReordered { FieldIds = new[] { 10L, 50L, 11L } } ); } @@ -664,7 +664,7 @@ namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization events.ShouldHaveSameEvents( new FieldDeleted { FieldId = NamedId.Of(10L, "f1") }, new FieldAdded { FieldId = NamedId.Of(50L, "f3"), Name = "f3", Partitioning = Partitioning.Invariant.Key, Properties = new StringFieldProperties() }, - new SchemaFieldsReordered { FieldIds = new List { 50, 11 } } + new SchemaFieldsReordered { FieldIds = new[] { 50L, 11L } } ); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TestUtils.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TestUtils.cs index c1a506196..809487a75 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TestUtils.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TestUtils.cs @@ -59,6 +59,7 @@ namespace Squidex.Domain.Apps.Core.TestHelpers new NamedStringIdConverter(), new PropertyPathConverter(), new RefTokenConverter(), + new RoleConverter(), new RolesConverter(), new RuleConverter(), new SchemaConverter(), diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs index 161e8aaa5..80e4c20d0 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs @@ -6,7 +6,6 @@ // ========================================================================== using System; -using System.Collections.Generic; using System.Threading.Tasks; using FakeItEasy; using Squidex.Domain.Apps.Core.Apps; @@ -18,6 +17,7 @@ using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Domain.Apps.Events.Apps; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Log; using Squidex.Shared.Users; using Xunit; @@ -529,7 +529,7 @@ namespace Squidex.Domain.Apps.Entities.Apps [Fact] public async Task UpdateLanguage_should_create_events_and_update_language() { - var command = new UpdateLanguage { Language = Language.DE, Fallback = new List { Language.EN } }; + var command = new UpdateLanguage { Language = Language.DE, Fallback = new[] { Language.EN } }; await ExecuteCreateAsync(); await ExecuteAddLanguageAsync(Language.DE); @@ -542,7 +542,7 @@ namespace Squidex.Domain.Apps.Entities.Apps LastEvents .ShouldHaveSameEvents( - CreateEvent(new AppLanguageUpdated { Language = Language.DE, Fallback = new List { Language.EN } }) + CreateEvent(new AppLanguageUpdated { Language = Language.DE, Fallback = new[] { Language.EN } }) ); } @@ -588,7 +588,7 @@ namespace Squidex.Domain.Apps.Entities.Apps [Fact] 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 ExecuteAddRoleAsync(); @@ -599,7 +599,7 @@ namespace Squidex.Domain.Apps.Entities.Apps LastEvents .ShouldHaveSameEvents( - CreateEvent(new AppRoleUpdated { Name = roleName, Permissions = new[] { "clients.read" } }) + CreateEvent(new AppRoleUpdated { Name = roleName, Permissions = command.Permissions, Properties = command.Properties }) ); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppSettingsSearchSourceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppSettingsSearchSourceTests.cs index ff94b275d..818b38e78 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppSettingsSearchSourceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppSettingsSearchSourceTests.cs @@ -32,7 +32,7 @@ namespace Squidex.Domain.Apps.Entities.Apps } [Fact] - public async Task Should_empty_if_nothing_matching() + public async Task Should_return_empty_if_nothing_matching() { var ctx = ContextWithPermission(); @@ -42,39 +42,38 @@ namespace Squidex.Domain.Apps.Entities.Apps } [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)) - .Returns("contents-url"); + A.CallTo(() => urlGenerator.DashboardUI(appId)) + .Returns("dashboard-url"); - var result = await sut.SearchAsync("content", ctx); + var result = await sut.SearchAsync("dashboard", ctx); result.Should().BeEquivalentTo( new SearchResults() - .Add("Contents", SearchResultType.Content, "contents-url")); + .Add("Dashboard", SearchResultType.Dashboard, "dashboard-url")); } [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(); - A.CallTo(() => urlGenerator.DashboardUI(appId)) - .Returns("dashboard-url"); - - var result = await sut.SearchAsync("dashboard", ctx); + var result = await sut.SearchAsync("assets", ctx); - result.Should().BeEquivalentTo( - new SearchResults() - .Add("Dashboard", SearchResultType.Dashboard, "dashboard-url")); + Assert.Empty(result); } [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)) .Returns("languages-url"); @@ -87,10 +86,22 @@ namespace Squidex.Domain.Apps.Entities.Apps } [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 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)) .Returns("patterns-url"); @@ -102,10 +113,22 @@ namespace Squidex.Domain.Apps.Entities.Apps } [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 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)) .Returns("schemas-url"); @@ -116,6 +139,16 @@ namespace Squidex.Domain.Apps.Entities.Apps .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] public async Task Should_return_assets_result_if_matching_and_permission_given() { @@ -323,7 +356,7 @@ namespace Squidex.Domain.Apps.Entities.Apps } [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(); @@ -337,13 +370,11 @@ namespace Squidex.Domain.Apps.Entities.Apps var claimsIdentity = new 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)); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppLanguagesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppLanguagesTests.cs index fe7bcd7cf..701b2d43b 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppLanguagesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppLanguagesTests.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Collections.Generic; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.TestHelpers; using Squidex.Domain.Apps.Entities.Apps.Commands; @@ -101,7 +100,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards [Fact] public void CanUpdateLanguage_should_throw_exception_if_fallback_language_defined_and_master() { - var command = new UpdateLanguage { Language = Language.EN, Fallback = new List { Language.DE } }; + var command = new UpdateLanguage { Language = Language.EN, Fallback = new[] { Language.DE } }; ValidationAssert.Throws(() => GuardAppLanguages.CanUpdate(languages, command), new ValidationError("Master language cannot have fallback languages.", "Fallback")); @@ -110,7 +109,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards [Fact] public void CanUpdateLanguage_should_throw_exception_if_language_has_invalid_fallback() { - var command = new UpdateLanguage { Language = Language.DE, Fallback = new List { Language.IT } }; + var command = new UpdateLanguage { Language = Language.DE, Fallback = new[] { Language.IT } }; ValidationAssert.Throws(() => GuardAppLanguages.CanUpdate(languages, command), new ValidationError("App does not have fallback language 'Italian'.", "Fallback")); @@ -127,7 +126,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards [Fact] public void CanUpdateLanguage_should_not_throw_exception_if_language_is_valid() { - var command = new UpdateLanguage { Language = Language.DE, Fallback = new List { Language.EN } }; + var command = new UpdateLanguage { Language = Language.DE, Fallback = new[] { Language.EN } }; GuardAppLanguages.CanUpdate(languages, command); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppRolesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppRolesTests.cs index 09c039837..b0af8856e 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppRolesTests.cs +++ b/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.TestHelpers; using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Validation; using Xunit; @@ -85,9 +86,11 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards { var roles_1 = roles_0.Add(roleName); + var clients_1 = clients.Add("1", new AppClient("client", "1", 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.")); } @@ -128,7 +131,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards { 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), new ValidationError("Permissions is required.", "Permissions")); @@ -153,6 +156,16 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards Assert.Throws(() => 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] public void CanUpdate_should_not_throw_exception_if_role_exist_with_valid_command() { diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/BulkUpdateCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/BulkUpdateCommandMiddlewareTests.cs index 0d87c2fb2..a0ed6e2cc 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/BulkUpdateCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/BulkUpdateCommandMiddlewareTests.cs @@ -6,7 +6,6 @@ // ========================================================================== using System; -using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using FakeItEasy; @@ -56,7 +55,7 @@ namespace Squidex.Domain.Apps.Entities.Contents [Fact] public async Task Should_do_nothing_if_jobs_is_empty() { - var command = new BulkUpdateContents { Jobs = new List() }; + var command = new BulkUpdateContents { Jobs = Array.Empty() }; var context = new CommandContext(command, commandBus); @@ -80,7 +79,7 @@ namespace Squidex.Domain.Apps.Entities.Contents var command = new BulkUpdateContents { - Jobs = new List + Jobs = new[] { new BulkUpdateJob { @@ -119,7 +118,7 @@ namespace Squidex.Domain.Apps.Entities.Contents var command = new BulkUpdateContents { - Jobs = new List + Jobs = new[] { new BulkUpdateJob { @@ -154,7 +153,7 @@ namespace Squidex.Domain.Apps.Entities.Contents var command = new BulkUpdateContents { - Jobs = new List + Jobs = new[] { new BulkUpdateJob { @@ -189,7 +188,7 @@ namespace Squidex.Domain.Apps.Entities.Contents var command = new BulkUpdateContents { - Jobs = new List + Jobs = new[] { new BulkUpdateJob { @@ -224,7 +223,7 @@ namespace Squidex.Domain.Apps.Entities.Contents var command = new BulkUpdateContents { - Jobs = new List + Jobs = new[] { new BulkUpdateJob { @@ -256,7 +255,7 @@ namespace Squidex.Domain.Apps.Entities.Contents var command = new BulkUpdateContents { - Jobs = new List + Jobs = new[] { new BulkUpdateJob { @@ -287,7 +286,7 @@ namespace Squidex.Domain.Apps.Entities.Contents var command = new BulkUpdateContents { - Jobs = new List + Jobs = new[] { new BulkUpdateJob { @@ -318,7 +317,7 @@ namespace Squidex.Domain.Apps.Entities.Contents var command = new BulkUpdateContents { - Jobs = new List + Jobs = new[] { new BulkUpdateJob { @@ -349,7 +348,7 @@ namespace Squidex.Domain.Apps.Entities.Contents var command = new BulkUpdateContents { - Jobs = new List + Jobs = new[] { new BulkUpdateJob { diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaTests.cs index 5c774feab..e8d81c88c 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaTests.cs +++ b/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 { AppId = appId, - Fields = new List + Fields = new[] { new UpsertSchemaField { @@ -72,7 +72,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards var command = new CreateSchema { AppId = appId, - Fields = new List + Fields = new[] { new UpsertSchemaField { @@ -95,7 +95,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards var command = new CreateSchema { AppId = appId, - Fields = new List + Fields = new[] { new UpsertSchemaField { @@ -119,7 +119,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards var command = new CreateSchema { AppId = appId, - Fields = new List + Fields = new[] { new UpsertSchemaField { @@ -142,7 +142,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards var command = new CreateSchema { AppId = appId, - Fields = new List + Fields = new[] { new UpsertSchemaField { @@ -171,14 +171,14 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards var command = new CreateSchema { AppId = appId, - Fields = new List + Fields = new[] { new UpsertSchemaField { Name = "array", Properties = new ArrayFieldProperties(), Partitioning = Partitioning.Invariant.Key, - Nested = new List + Nested = new[] { new UpsertSchemaNestedField { @@ -202,14 +202,14 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards var command = new CreateSchema { AppId = appId, - Fields = new List + Fields = new[] { new UpsertSchemaField { Name = "array", Properties = new ArrayFieldProperties(), Partitioning = Partitioning.Invariant.Key, - Nested = new List + Nested = new[] { new UpsertSchemaNestedField { @@ -233,14 +233,14 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards var command = new CreateSchema { AppId = appId, - Fields = new List + Fields = new[] { new UpsertSchemaField { Name = "array", Properties = new ArrayFieldProperties(), Partitioning = Partitioning.Invariant.Key, - Nested = new List + Nested = new[] { new UpsertSchemaNestedField { @@ -264,14 +264,14 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards var command = new CreateSchema { AppId = appId, - Fields = new List + Fields = new[] { new UpsertSchemaField { Name = "array", Properties = new ArrayFieldProperties(), Partitioning = Partitioning.Invariant.Key, - Nested = new List + Nested = new[] { new UpsertSchemaNestedField { @@ -296,14 +296,14 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards var command = new CreateSchema { AppId = appId, - Fields = new List + Fields = new[] { new UpsertSchemaField { Name = "array", Properties = new ArrayFieldProperties(), Partitioning = Partitioning.Invariant.Key, - Nested = new List + Nested = new[] { new UpsertSchemaNestedField { @@ -332,7 +332,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards var command = new CreateSchema { AppId = appId, - Fields = new List + Fields = new[] { new UpsertSchemaField { @@ -364,7 +364,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards { var command = new CreateSchema { - Fields = new List + Fields = new[] { new UpsertSchemaField { @@ -402,7 +402,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards { var command = new CreateSchema { - Fields = new List + Fields = new[] { new UpsertSchemaField { @@ -456,7 +456,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards var command = new CreateSchema { AppId = appId, - Fields = new List + Fields = new[] { new UpsertSchemaField { @@ -477,7 +477,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards Name = "field3", Properties = new ArrayFieldProperties(), Partitioning = Partitioning.Invariant.Key, - Nested = new List + Nested = new[] { new UpsertSchemaNestedField { @@ -575,7 +575,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards { var command = new ConfigureFieldRules { - FieldRules = new List + FieldRules = new[] { new FieldRuleCommand { Field = "field", Action = (FieldRuleAction)5 }, new FieldRuleCommand(), @@ -594,7 +594,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards { var command = new ConfigureFieldRules { - FieldRules = new List + FieldRules = new[] { new FieldRuleCommand { Field = "field1", Action = FieldRuleAction.Disable, Condition = "a == b" }, new FieldRuleCommand { Field = "field2" } @@ -634,7 +634,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards [Fact] public void CanReorder_should_throw_exception_if_field_ids_contains_invalid_id() { - var command = new ReorderFields { FieldIds = new List { 1, 3 } }; + var command = new ReorderFields { FieldIds = new[] { 1L, 3L } }; ValidationAssert.Throws(() => GuardSchema.CanReorder(command, schema_0), new ValidationError("Field ids do not cover all fields.", "FieldIds")); @@ -643,7 +643,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards [Fact] public void CanReorder_should_throw_exception_if_field_ids_do_not_covers_all_fields() { - var command = new ReorderFields { FieldIds = new List { 1 } }; + var command = new ReorderFields { FieldIds = new[] { 1L } }; ValidationAssert.Throws(() => GuardSchema.CanReorder(command, schema_0), new ValidationError("Field ids do not cover all fields.", "FieldIds")); @@ -661,7 +661,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards [Fact] public void CanReorder_should_throw_exception_if_parent_field_not_found() { - var command = new ReorderFields { FieldIds = new List { 1, 2 }, ParentFieldId = 99 }; + var command = new ReorderFields { FieldIds = new[] { 1L, 2L }, ParentFieldId = 99 }; Assert.Throws(() => GuardSchema.CanReorder(command, schema_0)); } @@ -669,7 +669,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards [Fact] public void CanReorder_should_not_throw_exception_if_field_ids_are_valid() { - var command = new ReorderFields { FieldIds = new List { 1, 2, 4 } }; + var command = new ReorderFields { FieldIds = new[] { 1L, 2L, 4L } }; GuardSchema.CanReorder(command, schema_0); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaCommandsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaCommandsTests.cs index ef81f7f73..19b479abc 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaCommandsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaCommandsTests.cs @@ -23,7 +23,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas { IsPublished = true, Properties = new SchemaProperties { Hints = "MyHints" }, - Fields = new List + Fields = new[] { new UpsertSchemaField { diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaDomainObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaDomainObjectTests.cs index d9950b497..b9ff1650a 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaDomainObjectTests.cs +++ b/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 fields = new List + var fields = new[] { new UpsertSchemaField { Name = "field1", Properties = ValidProperties() }, new UpsertSchemaField { Name = "field2", Properties = ValidProperties() }, @@ -89,7 +89,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas Name = "field3", Partitioning = Partitioning.Language.Key, Properties = new ArrayFieldProperties(), - Nested = new List + Nested = new[] { new UpsertSchemaNestedField { Name = "nested1", Properties = ValidProperties() }, new UpsertSchemaNestedField { Name = "nested2", Properties = ValidProperties() } @@ -161,7 +161,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas { var command = new ConfigureFieldRules { - FieldRules = new List + FieldRules = new[] { new FieldRuleCommand { Field = "field1" } } @@ -332,7 +332,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas [Fact] public async Task Reorder_should_create_events_and_reorder_fields() { - var command = new ReorderFields { FieldIds = new List { 2, 1 } }; + var command = new ReorderFields { FieldIds = new[] { 2L, 1L } }; await ExecuteCreateAsync(); await ExecuteAddFieldAsync("field1"); @@ -351,7 +351,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas [Fact] public async Task Reorder_should_create_events_and_reorder_nestedy_fields() { - var command = new ReorderFields { ParentFieldId = 1, FieldIds = new List { 3, 2 } }; + var command = new ReorderFields { ParentFieldId = 1, FieldIds = new[] { 3L, 2L } }; await ExecuteCreateAsync(); await ExecuteAddArrayFieldAsync(); diff --git a/backend/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs b/backend/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs index ec453dc68..b1c5a2bf5 100644 --- a/backend/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs +++ b/backend/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; @@ -20,6 +21,7 @@ using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Infrastructure.Security; +using Squidex.Shared; using Squidex.Shared.Identity; using Xunit; @@ -94,7 +96,7 @@ namespace Squidex.Web.Pipeline { var user = SetupUser(); - var app = CreateApp(appName, appUser: "user1"); + var app = CreateApp(appName); user.AddClaim(new Claim(OpenIdClaims.Subject, "user1")); user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.my-app")); @@ -104,8 +106,59 @@ namespace Squidex.Web.Pipeline 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.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); } @@ -231,28 +284,26 @@ namespace Squidex.Web.Pipeline { var appEntity = A.Fake(); + var contributors = AppContributors.Empty; + if (appUser != null) { - A.CallTo(() => appEntity.Contributors) - .Returns(AppContributors.Empty.Assign(appUser, Role.Owner)); - } - else - { - A.CallTo(() => appEntity.Contributors) - .Returns(AppContributors.Empty); + contributors = contributors.Assign(appUser, Role.Reader); } + var clients = AppClients.Empty; + if (appClient != null) { - A.CallTo(() => appEntity.Clients) - .Returns(AppClients.Empty.Add(appClient, "secret").Update(appClient, apiCallsLimit: apiCallsLimit, allowAnonymous: allowAnonymous)); - } - else - { - A.CallTo(() => appEntity.Clients) - .Returns(AppClients.Empty); + clients = clients.Add(appClient, "secret").Update(appClient, apiCallsLimit: apiCallsLimit, allowAnonymous: allowAnonymous); } + A.CallTo(() => appEntity.Contributors) + .Returns(contributors); + + A.CallTo(() => appEntity.Clients) + .Returns(clients); + A.CallTo(() => appEntity.Name) .Returns(name); diff --git a/frontend/app/features/apps/pages/apps-page.component.ts b/frontend/app/features/apps/pages/apps-page.component.ts index 4396e14cb..03162eced 100644 --- a/frontend/app/features/apps/pages/apps-page.component.ts +++ b/frontend/app/features/apps/pages/apps-page.component.ts @@ -7,6 +7,7 @@ import { Component, OnInit } from '@angular/core'; 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'; @Component({ @@ -48,7 +49,7 @@ export class AppsPageComponent implements OnInit { this.onboardingService.disable('dialog'); this.onboardingDialog.show(); } 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) .subscribe(result => { @@ -58,7 +59,7 @@ export class AppsPageComponent implements OnInit { this.newsDialog.show(); } - this.localStore.setInt('squidex.news.version', result.version); + this.localStore.setInt(Settings.Local.NEWS_VERSION, result.version); } }); } diff --git a/frontend/app/features/assets/pages/assets-page.component.ts b/frontend/app/features/assets/pages/assets-page.component.ts index e9985f3b4..3b5152c4e 100644 --- a/frontend/app/features/assets/pages/assets-page.component.ts +++ b/frontend/app/features/assets/pages/assets-page.component.ts @@ -7,6 +7,7 @@ import { Component, OnInit } from '@angular/core'; import { AssetsState, DialogModel, LocalStoreService, Queries, Query, ResourceOwner, Router2State, UIState } from '@app/shared'; +import { Settings } from '@app/shared/state/settings'; @Component({ selector: 'sqx-assets-page', @@ -31,7 +32,7 @@ export class AssetsPageComponent extends ResourceOwner implements OnInit { ) { super(); - this.isListView = this.localStore.getBoolean('squidex.assets.list-view'); + this.isListView = this.localStore.getBoolean(Settings.Local.ASSETS_MODE); } public ngOnInit() { @@ -57,6 +58,6 @@ export class AssetsPageComponent extends ResourceOwner implements OnInit { public changeView(isListView: boolean) { this.isListView = isListView; - this.localStore.setBoolean('squidex.assets.list-view', isListView); + this.localStore.setBoolean(Settings.Local.ASSETS_MODE, isListView); } } \ No newline at end of file diff --git a/frontend/app/features/content/pages/content/content-field.component.ts b/frontend/app/features/content/pages/content/content-field.component.ts index 4b466415d..2857e5b69 100644 --- a/frontend/app/features/content/pages/content/content-field.component.ts +++ b/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 { 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 { combineLatest } from 'rxjs/operators'; @@ -157,6 +157,6 @@ export class ContentFieldComponent implements OnChanges { } 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); } } \ No newline at end of file diff --git a/frontend/app/features/content/pages/content/content-section.component.ts b/frontend/app/features/content/pages/content/content-section.component.ts index 6af052635..b72424392 100644 --- a/frontend/app/features/content/pages/content/content-section.component.ts +++ b/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 { AppLanguageDto, EditContentForm, FieldForm, FieldSection, LocalStoreService, RootFieldDto, SchemaDto } from '@app/shared'; +import { AppLanguageDto, EditContentForm, FieldForm, FieldSection, LocalStoreService, RootFieldDto, SchemaDto, Settings } from '@app/shared'; @Component({ selector: 'sqx-content-section', @@ -65,6 +65,6 @@ export class ContentSectionComponent implements OnChanges { } 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); } } \ No newline at end of file diff --git a/frontend/app/features/content/pages/schemas/schemas-page.component.ts b/frontend/app/features/content/pages/schemas/schemas-page.component.ts index 85728cfb6..da163d70a 100644 --- a/frontend/app/features/content/pages/schemas/schemas-page.component.ts +++ b/frontend/app/features/content/pages/schemas/schemas-page.component.ts @@ -7,7 +7,7 @@ import { Component, OnInit } from '@angular/core'; import { FormControl } from '@angular/forms'; -import { LocalStoreService, SchemaCategory, SchemasState } from '@app/shared'; +import { LocalStoreService, SchemaCategory, SchemasState, Settings } from '@app/shared'; @Component({ selector: 'sqx-schemas-page', @@ -27,7 +27,7 @@ export class SchemasPageComponent implements OnInit { public readonly schemasState: SchemasState, private readonly localStore: LocalStoreService ) { - this.isCollapsed = localStore.getBoolean('content.schemas.collapsed'); + this.isCollapsed = localStore.getBoolean(Settings.Local.SCHEMAS_COLLAPSED); } public ngOnInit() { @@ -37,7 +37,7 @@ export class SchemasPageComponent implements OnInit { public toggle() { 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) { diff --git a/frontend/app/features/content/shared/forms/assets-editor.component.ts b/frontend/app/features/content/shared/forms/assets-editor.component.ts index b01dd4344..e5a8586d6 100644 --- a/frontend/app/features/content/shared/forms/assets-editor.component.ts +++ b/frontend/app/features/content/shared/forms/assets-editor.component.ts @@ -8,7 +8,7 @@ import { CdkDragDrop } from '@angular/cdk/drag-drop'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, OnInit } from '@angular/core'; 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 = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => AssetsEditorComponent), multi: true @@ -59,7 +59,7 @@ export class AssetsEditorComponent extends StatefulControlComponent implements } private configKey() { - return `squidex.schemas.${this.schema.id}.preview-button`; + return Settings.Local.SCHEMA_PREVIEW(this.schema.id); } } \ No newline at end of file diff --git a/frontend/app/features/dashboard/pages/dashboard-page.component.ts b/frontend/app/features/dashboard/pages/dashboard-page.component.ts index 9bafd3b4b..0483ba557 100644 --- a/frontend/app/features/dashboard/pages/dashboard-page.component.ts +++ b/frontend/app/features/dashboard/pages/dashboard-page.component.ts @@ -8,7 +8,7 @@ // tslint:disable: readonly-array 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 { switchMap } from 'rxjs/operators'; @@ -46,7 +46,7 @@ export class DashboardPageComponent extends ResourceOwner implements AfterViewIn ) { super(); - this.isStacked = localStore.getBoolean('dashboard.charts.stacked'); + this.isStacked = localStore.getBoolean(Settings.Local.DASHBOARD_CHART_STACKED); } public ngOnInit() { @@ -92,7 +92,7 @@ export class DashboardPageComponent extends ResourceOwner implements AfterViewIn } public changeIsStacked(value: boolean) { - this.localStore.setBoolean('dashboard.charts.stacked', value); + this.localStore.setBoolean(Settings.Local.DASHBOARD_CHART_STACKED, value); this.isStacked = value; } diff --git a/frontend/app/features/settings/pages/languages/language.component.ts b/frontend/app/features/settings/pages/languages/language.component.ts index b86c276ee..735e5bcf7 100644 --- a/frontend/app/features/settings/pages/languages/language.component.ts +++ b/frontend/app/features/settings/pages/languages/language.component.ts @@ -72,8 +72,6 @@ export class LanguageComponent implements OnChanges { this.languagesState.update(this.language, request) .subscribe(() => { this.editForm.submitCompleted({ noReset: true }); - - this.toggleEditing(); }, error => { this.editForm.submitFailed(error); }); diff --git a/frontend/app/features/settings/pages/languages/languages-page.component.html b/frontend/app/features/settings/pages/languages/languages-page.component.html index 9aac1132e..761230ad7 100644 --- a/frontend/app/features/settings/pages/languages/languages-page.component.html +++ b/frontend/app/features/settings/pages/languages/languages-page.component.html @@ -17,7 +17,10 @@
- + diff --git a/frontend/app/features/settings/pages/roles/role.component.html b/frontend/app/features/settings/pages/roles/role.component.html index 897743c3a..ed726380c 100644 --- a/frontend/app/features/settings/pages/roles/role.component.html +++ b/frontend/app/features/settings/pages/roles/role.component.html @@ -37,19 +37,27 @@
+

{{ 'roles.permissions' | sqxTranslate }}

+ + + {{ 'roles.permissionsDescription' | sqxTranslate }} + + -
-
- + + + + + +
+ - - -
- -
- + +
+ +
@@ -57,21 +65,65 @@ {{descriptions[role.name] | sqxTranslate}} -
- -
+ + + + +
+ +
- + +
-
+ +
+

{{ 'roles.properties' | sqxTranslate }}

+ + + {{ 'roles.propertiesDescription' | sqxTranslate }} + + +
+
+ + +
+
+ +
+
{{ 'common.schemas' | sqxTranslate }}
+ +
+
+ + +
+
+
+
diff --git a/frontend/app/features/settings/pages/roles/role.component.scss b/frontend/app/features/settings/pages/roles/role.component.scss index 00be886fc..e7bd49220 100644 --- a/frontend/app/features/settings/pages/roles/role.component.scss +++ b/frontend/app/features/settings/pages/roles/role.component.scss @@ -16,10 +16,34 @@ padding-right: 0; } +.form-control-empty { + background: 0; + border: 0; +} + .rule-name { @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 { color: $color-text; } diff --git a/frontend/app/features/settings/pages/roles/role.component.ts b/frontend/app/features/settings/pages/roles/role.component.ts index fc38dcedb..823a19df9 100644 --- a/frontend/app/features/settings/pages/roles/role.component.ts +++ b/frontend/app/features/settings/pages/roles/role.component.ts @@ -7,15 +7,31 @@ import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges, ViewChild } from '@angular/core'; 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', Editor: 'i18n:roles.defaults.editor', Owner: 'i18n:roles.default.owner', Reader: 'i18n:roles.default.reader' }; +type Property = { name: string, key: string }; + +const SIMPLE_PROPERTIES: ReadonlyArray = [{ + 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({ selector: 'sqx-role', styleUrls: ['./role.component.scss'], @@ -23,18 +39,27 @@ const Descriptions = { changeDetection: ChangeDetectionStrategy.OnPush }) export class RoleComponent implements OnChanges { + public readonly standalone = { standalone: true }; + @Input() public role: RoleDto; @Input() public allPermissions: AutocompleteSource; + @Input() + public schemas: ReadonlyArray; + @ViewChild('addInput', { static: false }) 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 addPermissionForm = new AddPermissionForm(this.formBuilder); @@ -51,15 +76,25 @@ export class RoleComponent implements OnChanges { if (changes['role']) { this.isEditable = this.role.canUpdate; + this.properties = this.role.properties; + this.editForm.load(this.role); this.editForm.setEnabled(this.isEditable); } } + public getProperty(name: string) { + return this.properties[name]; + } + public toggleEditing() { this.isEditing = !this.isEditing; } + public setProperty(name: string, value: boolean) { + this.properties[name] = value; + } + public delete() { this.rolesState.delete(this.role); } @@ -83,14 +118,20 @@ export class RoleComponent implements OnChanges { const value = this.editForm.submit(); if (value) { - this.rolesState.update(this.role, value) + this.rolesState.update(this.role, { ...value, properties: this.properties }) .subscribe(() => { this.editForm.submitCompleted({ noReset: true }); - - this.toggleEditing(); }, error => { this.editForm.submitFailed(error); }); } } + + public trackByProperty(_index: number, property: Property) { + return property.key; + } + + public trackBySchema(_index: number, schema: SchemaDto) { + return schema.id; + } } \ No newline at end of file diff --git a/frontend/app/features/settings/pages/roles/roles-page.component.html b/frontend/app/features/settings/pages/roles/roles-page.component.html index 1f8b85fea..5a3a8b1b9 100644 --- a/frontend/app/features/settings/pages/roles/roles-page.component.html +++ b/frontend/app/features/settings/pages/roles/roles-page.component.html @@ -17,7 +17,9 @@
- + + diff --git a/frontend/app/features/settings/pages/roles/roles-page.component.ts b/frontend/app/features/settings/pages/roles/roles-page.component.ts index 3e43b4eef..0a7384201 100644 --- a/frontend/app/features/settings/pages/roles/roles-page.component.ts +++ b/frontend/app/features/settings/pages/roles/roles-page.component.ts @@ -6,7 +6,7 @@ */ 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'; class PermissionsAutocomplete implements AutocompleteSource { @@ -32,11 +32,14 @@ export class RolesPageComponent implements OnInit { constructor( private readonly appsState: AppsState, public readonly rolesService: RolesService, - public readonly rolesState: RolesState + public readonly rolesState: RolesState, + public readonly schemasState: SchemasState ) { } public ngOnInit() { + this.schemasState.loadIfNotLoaded(); + this.rolesState.load(); } diff --git a/frontend/app/framework/angular/forms/editors/autocomplete.component.html b/frontend/app/framework/angular/forms/editors/autocomplete.component.html index db3bf9934..c06a52bc8 100644 --- a/frontend/app/framework/angular/forms/editors/autocomplete.component.html +++ b/frontend/app/framework/angular/forms/editors/autocomplete.component.html @@ -6,7 +6,8 @@ autocomplete="off" autocorrect="off" autocapitalize="off" - [class.form-underlined]="underlined" + [class.form-empty]="inputStyle === 'empty'" + [class.form-underlined]="inputStyle === 'underlined'" [class.form-icon]="!!icon" [formControl]="queryInput"> diff --git a/frontend/app/framework/angular/forms/editors/autocomplete.component.ts b/frontend/app/framework/angular/forms/editors/autocomplete.component.ts index a672ec83d..de35bc71b 100644 --- a/frontend/app/framework/angular/forms/editors/autocomplete.component.ts +++ b/frontend/app/framework/angular/forms/editors/autocomplete.component.ts @@ -56,6 +56,9 @@ export class AutocompleteComponent extends StatefulControlComponent implements super(changeDector, { selectedAssets: {}, 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 implements public changeView(isListView: boolean) { this.next(s => ({ ...s, isListView })); - this.localStore.setBoolean('squidex.assets.list-view', isListView); + this.localStore.setBoolean(Settings.Local.ASSETS_MODE, isListView); } } \ No newline at end of file diff --git a/frontend/app/shared/components/forms/geolocation-editor.component.ts b/frontend/app/shared/components/forms/geolocation-editor.component.ts index b69322344..1613de959 100644 --- a/frontend/app/shared/components/forms/geolocation-editor.component.ts +++ b/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 { 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 google: any; @@ -76,7 +76,7 @@ export class GeolocationEditorComponent extends StatefulControlComponent x.canReadContents && x.isPublished); + this.filteredSchemas = this.filteredSchemas.filter(x => !app.roleProperties[Settings.AppProperties.HIDE_CONTENTS(x.name)]); } if (this.schemasFilter) { diff --git a/frontend/app/shared/internal.ts b/frontend/app/shared/internal.ts index 3dc0c5ce4..7cd6d124b 100644 --- a/frontend/app/shared/internal.ts +++ b/frontend/app/shared/internal.ts @@ -66,6 +66,7 @@ export * from './state/rules.state'; export * from './state/schema-tag-source'; export * from './state/schemas.forms'; export * from './state/schemas.state'; +export * from './state/settings'; export * from './state/table-fields'; export * from './state/ui.state'; export * from './state/workflows.forms'; diff --git a/frontend/app/shared/services/apps.service.spec.ts b/frontend/app/shared/services/apps.service.spec.ts index 89e4432aa..326d135b9 100644 --- a/frontend/app/shared/services/apps.service.spec.ts +++ b/frontend/app/shared/services/apps.service.spec.ts @@ -227,6 +227,7 @@ describe('AppsService', () => { canAccessContent: id % 2 === 0, planName: 'Free', planUpgrade: 'Basic', + roleProperties: createProperties(id), version: id, _links: { schemas: { method: 'GET', href: '/schemas' } @@ -251,5 +252,14 @@ export function createApp(id: number, suffix = '') { id % 2 === 0, id % 2 === 0, 'Free', 'Basic', + createProperties(id), new Version(`${id}${suffix}`)); +} + +function createProperties(id: number) { + const result = {}; + + result[`property${id}`] = true; + + return result; } \ No newline at end of file diff --git a/frontend/app/shared/services/apps.service.ts b/frontend/app/shared/services/apps.service.ts index d55a05c32..d2b96e28a 100644 --- a/frontend/app/shared/services/apps.service.ts +++ b/frontend/app/shared/services/apps.service.ts @@ -48,6 +48,7 @@ export class AppDto { public readonly canAccessContent: boolean, public readonly planName: string | undefined, public readonly planUpgrade: string | undefined, + public readonly roleProperties: {}, public readonly version: Version ) { this._links = links; @@ -220,5 +221,6 @@ function parseApp(response: any) { response.canAccessContent, response.planName, response.planUpgrade, + response.roleProperties, new Version(response.version.toString())); } diff --git a/frontend/app/shared/services/roles.service.spec.ts b/frontend/app/shared/services/roles.service.spec.ts index 15f995c41..f3742db48 100644 --- a/frontend/app/shared/services/roles.service.spec.ts +++ b/frontend/app/shared/services/roles.service.spec.ts @@ -99,7 +99,7 @@ describe('RolesService', () => { it('should make put request to update role', inject([RolesService, HttpTestingController], (roleService: RolesService, httpMock: HttpTestingController) => { - const dto = { permissions: ['P4', 'P5'] }; + const dto = { permissions: ['P4', 'P5'], properties: createProperties(1) }; const resource: Resource = { _links: { @@ -162,7 +162,8 @@ describe('RolesService', () => { name: `name${id}`, numClients: id * 2, numContributors: id * 3, - permissions: [`permission${id}`], + permissions: createPermissions(id), + properties: createProperties(id), isDefaultRole: id % 2 === 0, _links: { update: { method: 'PUT', href: `/roles/id${id}` } @@ -190,5 +191,24 @@ export function createRole(id: number) { 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; } \ No newline at end of file diff --git a/frontend/app/shared/services/roles.service.ts b/frontend/app/shared/services/roles.service.ts index b873f180a..392b85568 100644 --- a/frontend/app/shared/services/roles.service.ts +++ b/frontend/app/shared/services/roles.service.ts @@ -30,6 +30,7 @@ export class RoleDto { public readonly numClients: number, public readonly numContributors: number, public readonly permissions: ReadonlyArray, + public readonly properties: {}, public readonly isDefaultRole: boolean ) { this._links = links; @@ -45,6 +46,7 @@ export interface CreateRoleDto { export interface UpdateRoleDto { readonly permissions: ReadonlyArray; + readonly properties: {}; } @Injectable() @@ -126,6 +128,7 @@ export function parseRoles(response: any) { item.numClients, item.numContributors, item.permissions, + item.properties, item.isDefaultRole)); const _links = response._links; diff --git a/frontend/app/shared/state/roles.forms.ts b/frontend/app/shared/state/roles.forms.ts index 1af14a3f2..13837aa1a 100644 --- a/frontend/app/shared/state/roles.forms.ts +++ b/frontend/app/shared/state/roles.forms.ts @@ -27,7 +27,7 @@ export class EditRoleForm extends Form { } public transformSubmit(value: any) { - return { permissions: value }; + return { permissions: value, properties: {} }; } public transformLoad(value: Partial) { diff --git a/frontend/app/shared/state/roles.state.spec.ts b/frontend/app/shared/state/roles.state.spec.ts index 78f87cc88..78954e613 100644 --- a/frontend/app/shared/state/roles.state.spec.ts +++ b/frontend/app/shared/state/roles.state.spec.ts @@ -93,7 +93,7 @@ describe('RolesState', () => { it('should update roles when role updated', () => { const updated = createRoles(4, 5); - const request = { permissions: ['P4', 'P5'] }; + const request = { permissions: ['P4', 'P5'], properties: {} }; rolesService.setup(x => x.putRole(app, oldRoles.items[1], request, version)) .returns(() => of(versioned(newVersion, updated))); diff --git a/frontend/app/shared/state/settings.ts b/frontend/app/shared/state/settings.ts new file mode 100644 index 000000000..6c9e091cc --- /dev/null +++ b/frontend/app/shared/state/settings.ts @@ -0,0 +1,28 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +export const Settings = { + AppProperties: { + HIDE_API: 'ui.api.hide', + HIDE_ASSETS: 'ui.assets.hide', + HIDE_CONTENTS: (schema: any) => `ui.contents.${schema}.hide`, + HIDE_SCHEMAS: 'ui.schemas.hide', + HIDE_SETTINGS: 'ui.settings.hide' + }, + Local: { + ASSETS_MODE: 'squidex.assets.list-view', + DASHBOARD_CHART_STACKED: 'dashboard.charts.stacked', + DISABLE_ONBOARDING: (key: any) => `squidex.onboarding.disable.${key}`, + FIELD_ALL: (schema: any, field: any) => `squidex.schemas.${schema}.fields.${field}.show-all`, + FIELD_COLLAPSED: (schema: any, field: any) => `squidex.schemas.${schema}.fields.${field}.closed`, + HIDE_MAP: 'hideMap', + NEWS_VERSION: 'squidex.news.version', + SCHEMA_CATEGORY_COLLAPSED: (category: any) => `squidex.schema.category.${category}.collapsed`, + SCHEMA_PREVIEW: (schema: any) => `squidex.schemas.${schema}.preview-button`, + SCHEMAS_COLLAPSED: 'content.schemas.collapsed' + } +}; \ No newline at end of file diff --git a/frontend/app/shell/pages/app/left-menu.component.html b/frontend/app/shell/pages/app/left-menu.component.html index c7c7fd9a8..98b873e03 100644 --- a/frontend/app/shell/pages/app/left-menu.component.html +++ b/frontend/app/shell/pages/app/left-menu.component.html @@ -1,5 +1,5 @@