diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs index 700fb4246..0e2770682 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs @@ -7,6 +7,7 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Security; +using System; using System.Collections.Generic; using System.Diagnostics.Contracts; using P = Squidex.Shared.Permissions; @@ -20,7 +21,7 @@ namespace Squidex.Domain.Apps.Core.Apps public const string Owner = "Owner"; public const string Reader = "Reader"; - private static readonly HashSet DefaultRolesSet = new HashSet + private static readonly HashSet DefaultRolesSet = new HashSet(StringComparer.OrdinalIgnoreCase) { Editor, Developer, @@ -54,6 +55,11 @@ namespace Squidex.Domain.Apps.Core.Apps return role != null && DefaultRolesSet.Contains(role); } + public static bool IsRole(string name, string expected) + { + return name != null && string.Equals(name, expected, StringComparison.OrdinalIgnoreCase); + } + public static Role CreateOwner(string app) { return new Role(Owner, diff --git a/src/Squidex.Web/Resource.cs b/src/Squidex.Web/Resource.cs index 59c4d10f9..d3eba847d 100644 --- a/src/Squidex.Web/Resource.cs +++ b/src/Squidex.Web/Resource.cs @@ -6,6 +6,7 @@ // ========================================================================== using Newtonsoft.Json; +using Squidex.Infrastructure; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; @@ -50,6 +51,10 @@ namespace Squidex.Web public void AddLink(string rel, string method, string href) { + Guard.NotNullOrEmpty(rel, nameof(rel)); + Guard.NotNullOrEmpty(href, nameof(href)); + Guard.NotNullOrEmpty(method, nameof(method)); + Links[rel] = new ResourceLink { Href = href, Method = method }; } } diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs index b2d924b77..15cd8f80a 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs @@ -42,12 +42,12 @@ namespace Squidex.Areas.Api.Controllers.Apps /// [HttpGet] [Route("apps/{app}/patterns/")] - [ProducesResponseType(typeof(AppPatternsDto), 200)] + [ProducesResponseType(typeof(PatternsDto), 200)] [ApiPermission(Permissions.AppCommon)] [ApiCosts(0)] public IActionResult GetPatterns(string app) { - var response = AppPatternsDto.FromApp(App, this); + var response = PatternsDto.FromApp(App, this); Response.Headers[HeaderNames.ETag] = App.Version.ToString(); @@ -66,7 +66,7 @@ namespace Squidex.Areas.Api.Controllers.Apps /// [HttpPost] [Route("apps/{app}/patterns/")] - [ProducesResponseType(typeof(AppPatternsDto), 200)] + [ProducesResponseType(typeof(PatternsDto), 200)] [ProducesResponseType(typeof(ErrorDto), 400)] [ApiPermission(Permissions.AppPatternsCreate)] [ApiCosts(1)] @@ -92,7 +92,7 @@ namespace Squidex.Areas.Api.Controllers.Apps /// [HttpPut] [Route("apps/{app}/patterns/{id}/")] - [ProducesResponseType(typeof(AppPatternsDto), 200)] + [ProducesResponseType(typeof(PatternsDto), 200)] [ProducesResponseType(typeof(ErrorDto), 400)] [ApiPermission(Permissions.AppPatternsUpdate)] [ApiCosts(1)] @@ -119,7 +119,7 @@ namespace Squidex.Areas.Api.Controllers.Apps /// [HttpDelete] [Route("apps/{app}/patterns/{id}/")] - [ProducesResponseType(typeof(AppPatternsDto), 200)] + [ProducesResponseType(typeof(PatternsDto), 200)] [ApiPermission(Permissions.AppPatternsDelete)] [ApiCosts(1)] public async Task DeletePattern(string app, Guid id) @@ -131,12 +131,12 @@ namespace Squidex.Areas.Api.Controllers.Apps return Ok(response); } - private async Task InvokeCommandAsync(ICommand command) + private async Task InvokeCommandAsync(ICommand command) { var context = await CommandBus.PublishAsync(command); var result = context.Result(); - var response = AppPatternsDto.FromApp(result, this); + var response = PatternsDto.FromApp(result, this); return response; } diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs index 2cce25530..3ec75ce9c 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs @@ -105,7 +105,7 @@ namespace Squidex.Areas.Api.Controllers.Apps /// Update an existing app role. /// /// The name of the app. - /// The name of the role to be updated. + /// The name of the role to be updated. /// Role to be updated for the app. /// /// 200 => Role updated. @@ -113,13 +113,13 @@ namespace Squidex.Areas.Api.Controllers.Apps /// 404 => Role or app not found. /// [HttpPut] - [Route("apps/{app}/roles/{role}/")] + [Route("apps/{app}/roles/{name}/")] [ProducesResponseType(typeof(RolesDto), 200)] [ApiPermission(Permissions.AppRolesUpdate)] [ApiCosts(1)] - public async Task UpdateRole(string app, string role, [FromBody] UpdateRoleDto request) + public async Task UpdateRole(string app, string name, [FromBody] UpdateRoleDto request) { - var command = request.ToCommand(role); + var command = request.ToCommand(name); var response = await InvokeCommandAsync(command); @@ -130,21 +130,21 @@ namespace Squidex.Areas.Api.Controllers.Apps /// Remove role from app. /// /// The name of the app. - /// The name of the role. + /// The name of the role. /// /// 200 => Role deleted. /// 400 => Role is in use by contributor or client or default role. /// 404 => Role or app not found. /// [HttpDelete] - [Route("apps/{app}/roles/{role}/")] + [Route("apps/{app}/roles/{name}/")] [ProducesResponseType(typeof(RolesDto), 200)] [ProducesResponseType(typeof(ErrorDto), 400)] [ApiPermission(Permissions.AppRolesDelete)] [ApiCosts(1)] - public async Task DeleteRole(string app, string role) + public async Task DeleteRole(string app, string name) { - var command = new DeleteRole { Name = role }; + var command = new DeleteRole { Name = name }; var response = await InvokeCommandAsync(command); diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs index ad7a0d072..1c2c28920 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs @@ -8,7 +8,6 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; using NodaTime; using Squidex.Areas.Api.Controllers.Assets; using Squidex.Areas.Api.Controllers.Backups; @@ -18,7 +17,6 @@ using Squidex.Areas.Api.Controllers.Rules; using Squidex.Areas.Api.Controllers.Schemas; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps.Services; -using Squidex.Infrastructure; using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Security; using Squidex.Shared; @@ -59,7 +57,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models /// /// The permission level of the user. /// - public string[] Permissions { get; set; } + public IEnumerable Permissions { get; set; } /// /// Indicates if the user can access the api. @@ -87,7 +85,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models var result = SimpleMapper.Map(app, new AppDto()); - result.Permissions = permissions.ToIds().ToArray(); + result.Permissions = permissions.ToIds(); result.PlanName = plans.GetPlanForApp(app)?.Name; result.CanAccessApi = controller.HasPermission(AllPermissions.AppApi, app.Name, "*", permissions); diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/AppPatternDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/PatternDto.cs similarity index 58% rename from src/Squidex/Areas/Api/Controllers/Apps/Models/AppPatternDto.cs rename to src/Squidex/Areas/Api/Controllers/Apps/Models/PatternDto.cs index 335c29a8d..1377aee3e 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/AppPatternDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/Models/PatternDto.cs @@ -9,16 +9,17 @@ using System; using System.ComponentModel.DataAnnotations; using Squidex.Domain.Apps.Core.Apps; using Squidex.Infrastructure.Reflection; +using Squidex.Shared; using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Apps.Models { - public sealed class AppPatternDto : Resource + public sealed class PatternDto : Resource { /// /// Unique id of the pattern. /// - public Guid PatternId { get; set; } + public Guid Id { get; set; } /// /// The name of the suggestion. @@ -37,15 +38,27 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models /// public string Message { get; set; } - public static AppPatternDto FromPattern(Guid id, AppPattern pattern, ApiController controller, string app) + public static PatternDto FromPattern(Guid id, AppPattern pattern, ApiController controller, string app) { - var result = SimpleMapper.Map(pattern, new AppPatternDto { PatternId = id }); + var result = SimpleMapper.Map(pattern, new PatternDto { Id = id }); return result.CreateLinks(controller, app); } - private AppPatternDto CreateLinks(ApiController controller, string app) + private PatternDto CreateLinks(ApiController controller, string app) { + var values = new { app, id = Id }; + + if (controller.HasPermission(Permissions.AppPatternsUpdate, app)) + { + AddPutLink("update", controller.Url(x => nameof(x.UpdatePattern), values)); + } + + if (controller.HasPermission(Permissions.AppPatternsDelete, app)) + { + AddDeleteLink("delete", controller.Url(x => nameof(x.DeletePattern), values)); + } + return this; } } diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/AppPatternsDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/PatternsDto.cs similarity index 50% rename from src/Squidex/Areas/Api/Controllers/Apps/Models/AppPatternsDto.cs rename to src/Squidex/Areas/Api/Controllers/Apps/Models/PatternsDto.cs index b0e03a577..ca9a2605a 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/AppPatternsDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/Models/PatternsDto.cs @@ -8,30 +8,40 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Shared; using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Apps.Models { - public sealed class AppPatternsDto : Resource + public sealed class PatternsDto : Resource { /// /// The patterns. /// [Required] - public AppPatternDto[] Items { get; set; } + public PatternDto[] Items { get; set; } - public static AppPatternsDto FromApp(IAppEntity app, ApiController controller) + public static PatternsDto FromApp(IAppEntity app, ApiController controller) { - var result = new AppPatternsDto + var result = new PatternsDto { - Items = app.Patterns.Select(x => AppPatternDto.FromPattern(x.Key, x.Value, controller, app.Name)).ToArray() + Items = app.Patterns.Select(x => PatternDto.FromPattern(x.Key, x.Value, controller, app.Name)).ToArray() }; return result.CreateLinks(controller, app.Name); } - private AppPatternsDto CreateLinks(ApiController controller, string app) + private PatternsDto CreateLinks(ApiController controller, string app) { + var values = new { app }; + + AddSelfLink(controller.Url(x => nameof(x.GetPatterns), values)); + + if (controller.HasPermission(Permissions.AppPatternsCreate, app)) + { + AddPostLink("create", controller.Url(x => nameof(x.PostPattern), values)); + } + return this; } } diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/RoleDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/RoleDto.cs index 856e47ec7..f703c3cf5 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/RoleDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/Models/RoleDto.cs @@ -5,17 +5,17 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Web; +using AllPermissions = Squidex.Shared.Permissions; namespace Squidex.Areas.Api.Controllers.Apps.Models { - public sealed class RoleDto + public sealed class RoleDto : Resource { /// /// The role name. @@ -33,6 +33,11 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models /// public int NumContributors { get; set; } + /// + /// Indicates if the role is an builtin default role. + /// + public bool IsDefaultRole { get; set; } + /// /// Associated list of permissions. /// @@ -46,16 +51,32 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models var result = new RoleDto { Name = role.Name, - NumClients = app.Clients.Count(x => string.Equals(x.Value.Role, role.Name, StringComparison.OrdinalIgnoreCase)), - NumContributors = app.Contributors.Count(x => string.Equals(x.Value, role.Name, StringComparison.OrdinalIgnoreCase)), - Permissions = permissions.ToIds() + NumClients = app.Clients.Count(x => Role.IsRole(x.Value.Role, role.Name)), + NumContributors = app.Contributors.Count(x => Role.IsRole(x.Value, role.Name)), + Permissions = permissions.ToIds(), + IsDefaultRole = Role.IsDefaultRole(role.Name) }; return result.CreateLinks(controller, app.Name); } - private RoleDto CreateLinks(ApiController controller, string name) + private RoleDto CreateLinks(ApiController controller, string app) { + var values = new { app, name = Name }; + + if (!IsDefaultRole) + { + if (controller.HasPermission(AllPermissions.AppRolesUpdate, app) && NumClients == 0 && NumContributors == 0) + { + AddPutLink("update", controller.Url(x => nameof(x.UpdateRole), values)); + } + + if (controller.HasPermission(AllPermissions.AppRolesDelete, app)) + { + AddDeleteLink("delete", controller.Url(x => nameof(x.DeleteRole), values)); + } + } + return this; } } diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/RolesDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/RolesDto.cs index 1e1b6b268..b43c32041 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/RolesDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/Models/RolesDto.cs @@ -8,6 +8,7 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Shared; using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Apps.Models @@ -24,7 +25,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models { var result = new RolesDto { - Items = app.Roles.Values.Select(x => RoleDto.FromRole(x, app, controller)).ToArray() + Items = app.Roles.Values.Select(x => RoleDto.FromRole(x, app, controller)).OrderBy(x => x.Name).ToArray() }; return result.CreateLinks(controller, app.Name); @@ -32,6 +33,15 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models private RolesDto CreateLinks(ApiController controller, string app) { + var values = new { app }; + + AddSelfLink(controller.Url(x => nameof(x.GetRoles), values)); + + if (controller.HasPermission(Permissions.AppRolesCreate, app)) + { + AddPostLink("create", controller.Url(x => nameof(x.PostRole), values)); + } + return this; } } diff --git a/src/Squidex/app/features/settings/pages/patterns/pattern.component.html b/src/Squidex/app/features/settings/pages/patterns/pattern.component.html index 8ba33c1cf..6387d57ca 100644 --- a/src/Squidex/app/features/settings/pages/patterns/pattern.component.html +++ b/src/Squidex/app/features/settings/pages/patterns/pattern.component.html @@ -20,11 +20,12 @@
- - - +
@@ -43,14 +44,14 @@ -
+
-
+
diff --git a/src/Squidex/app/features/settings/pages/roles/role.component.ts b/src/Squidex/app/features/settings/pages/roles/role.component.ts index 385fe6cf2..5bff6c267 100644 --- a/src/Squidex/app/features/settings/pages/roles/role.component.ts +++ b/src/Squidex/app/features/settings/pages/roles/role.component.ts @@ -14,17 +14,11 @@ import { AutocompleteSource, EditPermissionsForm, fadeAnimation, + hasAnyLink, RoleDto, RolesState } from '@app/shared'; -const DEFAULT_ROLES = [ - 'Owner', - 'Developer', - 'Editor', - 'Reader' -]; - @Component({ selector: 'sqx-role', styleUrls: ['./role.component.scss'], @@ -44,7 +38,7 @@ export class RoleComponent implements OnChanges { public addPermissionInput: AutocompleteComponent; public isEditing = false; - public isDefaultRole = false; + public isEditable = false; public addPermissionForm = new AddPermissionForm(this.formBuilder); @@ -57,11 +51,11 @@ export class RoleComponent implements OnChanges { } public ngOnChanges() { - this.isDefaultRole = DEFAULT_ROLES.indexOf(this.role.name) >= 0; + this.isEditable = hasAnyLink(this.role, 'update'); this.editForm.load(this.role.permissions); - if (this.isDefaultRole) { + if (!this.isEditable) { this.editForm.form.disable(); } } diff --git a/src/Squidex/app/features/settings/pages/roles/roles-page.component.html b/src/Squidex/app/features/settings/pages/roles/roles-page.component.html index a6fab661b..a6af88be0 100644 --- a/src/Squidex/app/features/settings/pages/roles/roles-page.component.html +++ b/src/Squidex/app/features/settings/pages/roles/roles-page.component.html @@ -17,7 +17,7 @@ -