Browse Source

Some progress.

pull/363/head
Sebastian 7 years ago
parent
commit
242df48c18
  1. 2
      src/Squidex.Shared/Permissions.cs
  2. 18
      src/Squidex.Web/PermissionExtensions.cs
  3. 2
      src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs
  4. 4
      src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs
  5. 91
      src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs
  6. 2
      src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs
  7. 70
      src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDetailsDto.cs
  8. 20
      src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs
  9. 4
      src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs
  10. 39
      src/Squidex/Areas/Api/Controllers/Users/Models/ResourcesDto.cs
  11. 17
      src/Squidex/Areas/Api/Controllers/Users/UsersController.cs
  12. 8
      src/Squidex/app/features/administration/administration-area.component.html
  13. 6
      src/Squidex/app/features/administration/administration-area.component.ts
  14. 2
      src/Squidex/app/features/dashboard/pages/dashboard-page.component.html
  15. 5
      src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts
  16. 18
      src/Squidex/app/features/settings/settings-area.component.html
  17. 20
      src/Squidex/app/framework/angular/http/hateos.pipes.ts
  18. 1
      src/Squidex/app/framework/internal.ts
  19. 3
      src/Squidex/app/framework/module.ts
  20. 145
      src/Squidex/app/framework/utils/permission.spec.ts
  21. 116
      src/Squidex/app/framework/utils/permission.ts
  22. 2
      src/Squidex/app/shared/components/asset-uploader.component.html
  23. 133
      src/Squidex/app/shared/components/permission.directive.ts
  24. 2
      src/Squidex/app/shared/components/schema-category.component.html
  25. 1
      src/Squidex/app/shared/declarations.ts
  26. 3
      src/Squidex/app/shared/module.ts
  27. 7
      src/Squidex/app/shared/services/apps.service.spec.ts
  28. 37
      src/Squidex/app/shared/services/apps.service.ts
  29. 21
      src/Squidex/app/shared/services/auth.service.ts
  30. 28
      src/Squidex/app/shared/services/users.service.spec.ts
  31. 21
      src/Squidex/app/shared/services/users.service.ts
  32. 9
      src/Squidex/app/shared/state/apps.state.spec.ts
  33. 9
      src/Squidex/app/shared/state/apps.state.ts
  34. 23
      src/Squidex/app/shared/state/ui.state.spec.ts
  35. 37
      src/Squidex/app/shared/state/ui.state.ts
  36. 14
      src/Squidex/app/shell/pages/app/left-menu.component.html
  37. 3
      src/Squidex/app/shell/pages/app/left-menu.component.ts
  38. 24
      src/Squidex/app/shell/pages/internal/apps-menu.component.html
  39. 8
      src/Squidex/app/shell/pages/internal/profile-menu.component.html
  40. 4
      src/Squidex/app/shell/pages/internal/profile-menu.component.ts

2
src/Squidex.Shared/Permissions.cs

@ -79,7 +79,6 @@ namespace Squidex.Shared
public const string AppRolesDelete = "squidex.apps.{app}.roles.delete"; public const string AppRolesDelete = "squidex.apps.{app}.roles.delete";
public const string AppPatterns = "squidex.apps.{app}.patterns"; public const string AppPatterns = "squidex.apps.{app}.patterns";
public const string AppPatternsRead = "squidex.apps.{app}.patterns.read";
public const string AppPatternsCreate = "squidex.apps.{app}.patterns.create"; public const string AppPatternsCreate = "squidex.apps.{app}.patterns.create";
public const string AppPatternsUpdate = "squidex.apps.{app}.patterns.update"; public const string AppPatternsUpdate = "squidex.apps.{app}.patterns.update";
public const string AppPatternsDelete = "squidex.apps.{app}.patterns.delete"; public const string AppPatternsDelete = "squidex.apps.{app}.patterns.delete";
@ -117,7 +116,6 @@ namespace Squidex.Shared
public const string AppContents = "squidex.apps.{app}.contents.{name}"; public const string AppContents = "squidex.apps.{app}.contents.{name}";
public const string AppContentsRead = "squidex.apps.{app}.contents.{name}.read"; public const string AppContentsRead = "squidex.apps.{app}.contents.{name}.read";
public const string AppContentsGraphQL = "squidex.apps.{app}.contents.{name}.graphql";
public const string AppContentsCreate = "squidex.apps.{app}.contents.{name}.create"; public const string AppContentsCreate = "squidex.apps.{app}.contents.{name}.create";
public const string AppContentsUpdate = "squidex.apps.{app}.contents.{name}.update"; public const string AppContentsUpdate = "squidex.apps.{app}.contents.{name}.update";
public const string AppContentsDiscard = "squidex.apps.{app}.contents.{name}.discard"; public const string AppContentsDiscard = "squidex.apps.{app}.contents.{name}.discard";

18
src/Squidex.Web/PermissionExtensions.cs

@ -24,7 +24,7 @@ namespace Squidex.Web
} }
} }
public static PermissionSet GetPermissions(this HttpContext httpContext) public static PermissionSet Permissions(this HttpContext httpContext)
{ {
var feature = httpContext.Features.Get<PermissionFeature>(); var feature = httpContext.Features.Get<PermissionFeature>();
@ -38,22 +38,22 @@ namespace Squidex.Web
return feature.Permissions; return feature.Permissions;
} }
public static bool HasPermission(this HttpContext httpContext, Permission permission) public static bool HasPermission(this HttpContext httpContext, Permission permission, PermissionSet permissions = null)
{ {
return httpContext.GetPermissions().Includes(permission); return httpContext.Permissions().Includes(permission) || permission?.Includes(permission) == true;
} }
public static bool HasPermission(this HttpContext httpContext, string id, string app = "*", string schema = "*") public static bool HasPermission(this HttpContext httpContext, string id, string app = "*", string schema = "*", PermissionSet permissions = null)
{ {
return httpContext.GetPermissions().Includes(Permissions.ForApp(id, app, schema)); return httpContext.HasPermission(Shared.Permissions.ForApp(id, app, schema), permissions);
} }
public static bool HasPermission(this ApiController controller, Permission permission) public static bool HasPermission(this ApiController controller, Permission permission, PermissionSet permissions = null)
{ {
return controller.HttpContext.GetPermissions().Includes(permission); return controller.HttpContext.HasPermission(permission, permissions);
} }
public static bool HasPermission(this ApiController controller, string id, string app = "*", string schema = "*") public static bool HasPermission(this ApiController controller, string id, string app = "*", string schema = "*", PermissionSet permissions = null)
{ {
if (app == "*") if (app == "*")
{ {
@ -71,7 +71,7 @@ namespace Squidex.Web
} }
} }
return controller.HttpContext.GetPermissions().Includes(Permissions.ForApp(id, app, schema)); return controller.HasPermission(Shared.Permissions.ForApp(id, app, schema), permissions);
} }
} }
} }

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

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

4
src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs

@ -58,11 +58,11 @@ namespace Squidex.Areas.Api.Controllers.Apps
public async Task<IActionResult> GetApps() public async Task<IActionResult> GetApps()
{ {
var userOrClientId = HttpContext.User.UserOrClientId(); var userOrClientId = HttpContext.User.UserOrClientId();
var userPermissions = HttpContext.User.Permissions(); var userPermissions = HttpContext.Permissions();
var entities = await appProvider.GetUserApps(userOrClientId, userPermissions); var entities = await appProvider.GetUserApps(userOrClientId, userPermissions);
var response = entities.ToArray(a => AppDto.FromApp(a, userOrClientId, userPermissions, appPlansProvider)); var response = entities.ToArray(a => AppDto.FromApp(a, userOrClientId, userPermissions, appPlansProvider, this));
Response.Headers[HeaderNames.ETag] = response.ToManyEtag(); Response.Headers[HeaderNames.ETag] = response.ToManyEtag();

91
src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs

@ -9,6 +9,11 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using NodaTime; using NodaTime;
using Squidex.Areas.Api.Controllers.Assets;
using Squidex.Areas.Api.Controllers.Backups;
using Squidex.Areas.Api.Controllers.Plans;
using Squidex.Areas.Api.Controllers.Rules;
using Squidex.Areas.Api.Controllers.Schemas;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.Services; using Squidex.Domain.Apps.Entities.Apps.Services;
using Squidex.Infrastructure; using Squidex.Infrastructure;
@ -16,10 +21,11 @@ using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Security;
using Squidex.Shared; using Squidex.Shared;
using Squidex.Web; using Squidex.Web;
using AllPermissions = Squidex.Shared.Permissions;
namespace Squidex.Areas.Api.Controllers.Apps.Models namespace Squidex.Areas.Api.Controllers.Apps.Models
{ {
public sealed class AppDto : IGenerateETag public sealed class AppDto : Resource, IGenerateETag
{ {
/// <summary> /// <summary>
/// The name of the app. /// The name of the app.
@ -63,7 +69,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// </summary> /// </summary>
public string PlanUpgrade { get; set; } public string PlanUpgrade { get; set; }
public static AppDto FromApp(IAppEntity app, string userId, PermissionSet userPermissions, IAppPlansProvider plans) public static AppDto FromApp(IAppEntity app, string userId, PermissionSet userPermissions, IAppPlansProvider plans, ApiController controller)
{ {
var permissions = new List<Permission>(); var permissions = new List<Permission>();
@ -77,13 +83,84 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
permissions.AddRange(userPermissions.ToAppPermissions(app.Name)); permissions.AddRange(userPermissions.ToAppPermissions(app.Name));
} }
var response = SimpleMapper.Map(app, new AppDto()); var result = SimpleMapper.Map(app, new AppDto());
response.Permissions = permissions.ToArray(x => x.Id); result.Permissions = permissions.ToArray(x => x.Id);
response.PlanName = plans.GetPlanForApp(app)?.Name; result.PlanName = plans.GetPlanForApp(app)?.Name;
response.PlanUpgrade = plans.GetPlanUpgradeForApp(app)?.Name;
return response; if (controller.HasPermission(AllPermissions.AppPlansChange, app.Name))
{
result.PlanUpgrade = plans.GetPlanUpgradeForApp(app)?.Name;
}
return CreateLinks(result, controller, new PermissionSet(permissions));
}
private static AppDto CreateLinks(AppDto result, ApiController controller, PermissionSet permissions)
{
var values = new { app = result.Name };
if (controller.HasPermission(AllPermissions.AppDelete, result.Name, permissions: permissions))
{
result.AddDeleteLink("delete", controller.Url<AppsController>(x => nameof(x.DeleteApp), values));
}
if (controller.HasPermission(AllPermissions.AppAssetsRead, result.Name, permissions: permissions))
{
result.AddGetLink("assets", controller.Url<AssetsController>(x => nameof(x.GetAssets), values));
}
if (controller.HasPermission(AllPermissions.AppBackupsRead, result.Name, permissions: permissions))
{
result.AddGetLink("backups", controller.Url<BackupsController>(x => nameof(x.GetJobs), values));
}
if (controller.HasPermission(AllPermissions.AppClientsRead, result.Name, permissions: permissions))
{
result.AddGetLink("clients", controller.Url<AppClientsController>(x => nameof(x.GetClients), values));
}
if (controller.HasPermission(AllPermissions.AppContributorsRead, result.Name, permissions: permissions))
{
result.AddGetLink("contributors", controller.Url<AppContributorsController>(x => nameof(x.GetContributors), values));
}
if (controller.HasPermission(AllPermissions.AppCommon, result.Name, permissions: permissions))
{
result.AddGetLink("languages", controller.Url<AppLanguagesController>(x => nameof(x.GetLanguages), values));
}
if (controller.HasPermission(AllPermissions.AppCommon, result.Name, permissions: permissions))
{
result.AddGetLink("patterns", controller.Url<AppPatternsController>(x => nameof(x.GetPatterns), values));
}
if (controller.HasPermission(AllPermissions.AppPlansRead, result.Name, permissions: permissions))
{
result.AddGetLink("plans", controller.Url<AppPlansController>(x => nameof(x.GetPlans), values));
}
if (controller.HasPermission(AllPermissions.AppRolesRead, result.Name, permissions: permissions))
{
result.AddGetLink("roles", controller.Url<AppRolesController>(x => nameof(x.GetRoles), values));
}
if (controller.HasPermission(AllPermissions.AppRulesRead, result.Name, permissions: permissions))
{
result.AddGetLink("rules", controller.Url<RulesController>(x => nameof(x.GetRules), values));
}
if (controller.HasPermission(AllPermissions.AppCommon, result.Name, permissions: permissions))
{
result.AddGetLink("schemas", controller.Url<SchemasController>(x => nameof(x.GetSchemas), values));
}
if (controller.HasPermission(AllPermissions.AppSchemasCreate, result.Name, permissions: permissions))
{
result.AddPostLink("schemas/create", controller.Url<SchemasController>(x => nameof(x.PostSchema), values));
}
return result;
} }
} }
} }

2
src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs

@ -74,7 +74,7 @@ namespace Squidex.Areas.Api.Controllers.Plans
[ProducesResponseType(typeof(ErrorDto), 400)] [ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppPlansChange)] [ApiPermission(Permissions.AppPlansChange)]
[ApiCosts(0)] [ApiCosts(0)]
public async Task<IActionResult> ChangePlanAsync(string app, [FromBody] ChangePlanDto request) public async Task<IActionResult> PutPlan(string app, [FromBody] ChangePlanDto request)
{ {
var context = await CommandBus.PublishAsync(request.ToCommand()); var context = await CommandBus.PublishAsync(request.ToCommand());

70
src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDetailsDto.cs

@ -5,49 +5,20 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using NodaTime;
using Squidex.Areas.Api.Controllers.Schemas.Models.Converters; using Squidex.Areas.Api.Controllers.Schemas.Models.Converters;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Schemas.Models namespace Squidex.Areas.Api.Controllers.Schemas.Models
{ {
public sealed class SchemaDetailsDto public sealed class SchemaDetailsDto : SchemaDto
{ {
private static readonly Dictionary<string, string> EmptyPreviewUrls = new Dictionary<string, string>(); private static readonly Dictionary<string, string> EmptyPreviewUrls = new Dictionary<string, string>();
/// <summary>
/// The id of the schema.
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// The name of the schema. Unique within the app.
/// </summary>
[Required]
[RegularExpression("^[a-z0-9]+(\\-[a-z0-9]+)*$")]
public string Name { get; set; }
/// <summary>
/// The name of the category.
/// </summary>
public string Category { get; set; }
/// <summary>
/// Indicates if the schema is a singleton.
/// </summary>
public bool IsSingleton { get; set; }
/// <summary>
/// Indicates if the schema is published.
/// </summary>
public bool IsPublished { get; set; }
/// <summary> /// <summary>
/// The scripts. /// The scripts.
/// </summary> /// </summary>
@ -64,40 +35,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
[Required] [Required]
public List<FieldDto> Fields { get; set; } public List<FieldDto> Fields { get; set; }
/// <summary> public static SchemaDetailsDto FromSchemaWithDetails(ISchemaEntity schema, ApiController controller, string app)
/// The schema properties.
/// </summary>
[Required]
public SchemaPropertiesDto Properties { get; set; } = new SchemaPropertiesDto();
/// <summary>
/// The user that has created the schema.
/// </summary>
[Required]
public RefToken CreatedBy { get; set; }
/// <summary>
/// The user that has updated the schema.
/// </summary>
[Required]
public RefToken LastModifiedBy { get; set; }
/// <summary>
/// The date and time when the schema has been created.
/// </summary>
public Instant Created { get; set; }
/// <summary>
/// The date and time when the schema has been modified last.
/// </summary>
public Instant LastModified { get; set; }
/// <summary>
/// The version of the schema.
/// </summary>
public long Version { get; set; }
public static SchemaDetailsDto FromSchema(ISchemaEntity schema)
{ {
var response = new SchemaDetailsDto(); var response = new SchemaDetailsDto();
@ -147,7 +85,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
response.Fields.Add(fieldDto); response.Fields.Add(fieldDto);
} }
return response; return CreateLinks(response, controller, app);
} }
} }
} }

20
src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs

@ -8,14 +8,16 @@
using System; using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using NodaTime; using NodaTime;
using Squidex.Areas.Api.Controllers.Contents;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using Squidex.Shared;
using Squidex.Web; using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Schemas.Models namespace Squidex.Areas.Api.Controllers.Schemas.Models
{ {
public sealed class SchemaDto : IGenerateETag public class SchemaDto : Resource, IGenerateETag
{ {
/// <summary> /// <summary>
/// The id of the schema. /// The id of the schema.
@ -77,7 +79,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
/// </summary> /// </summary>
public long Version { get; set; } public long Version { get; set; }
public static SchemaDto FromSchema(ISchemaEntity schema) public static SchemaDto FromSchema(ISchemaEntity schema, ApiController controller, string app)
{ {
var response = new SchemaDto { Properties = new SchemaPropertiesDto() }; var response = new SchemaDto { Properties = new SchemaPropertiesDto() };
@ -85,6 +87,20 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
SimpleMapper.Map(schema.SchemaDef, response); SimpleMapper.Map(schema.SchemaDef, response);
SimpleMapper.Map(schema.SchemaDef.Properties, response.Properties); SimpleMapper.Map(schema.SchemaDef.Properties, response.Properties);
return CreateLinks(response, controller, app);
}
protected static T CreateLinks<T>(T response, ApiController controller, string app) where T : SchemaDto
{
var values = new { app, name = response.Name };
response.AddSelfLink(controller.Url<SchemasController>(x => nameof(x.GetSchema), values));
if (controller.HasPermission(Permissions.AppContentsRead, app, response.Name))
{
response.AddGetLink("contents", controller.Url<ContentsController>(x => nameof(x.GetContents), values));
}
return response; return response;
} }
} }

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

@ -51,7 +51,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
{ {
var schemas = await appProvider.GetSchemasAsync(AppId); var schemas = await appProvider.GetSchemasAsync(AppId);
var response = schemas.ToArray(SchemaDto.FromSchema); var response = schemas.ToArray(x => SchemaDto.FromSchema(x, this, app));
Response.Headers[HeaderNames.ETag] = response.ToManyEtag(); Response.Headers[HeaderNames.ETag] = response.ToManyEtag();
@ -90,7 +90,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
return NotFound(); return NotFound();
} }
var response = SchemaDetailsDto.FromSchema(entity); var response = SchemaDetailsDto.FromSchemaWithDetails(entity, this, app);
Response.Headers[HeaderNames.ETag] = entity.Version.ToString(); Response.Headers[HeaderNames.ETag] = entity.Version.ToString();

39
src/Squidex/Areas/Api/Controllers/Users/Models/ResourcesDto.cs

@ -0,0 +1,39 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Areas.Api.Controllers.Backups;
using Squidex.Areas.Api.Controllers.EventConsumers;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Users.Models
{
public sealed class ResourcesDto : Resource
{
public static ResourcesDto FromController(ApiController controller)
{
var result = new ResourcesDto();
if (controller.HasPermission(Permissions.AdminEventsRead))
{
result.AddGetLink("admin/eventConsumers", controller.Url<EventConsumersController>(x => nameof(x.GetEventConsumers)));
}
if (controller.HasPermission(Permissions.AdminRestoreRead))
{
result.AddGetLink("admin/restore", controller.Url<RestoreController>(x => nameof(x.GetJob)));
}
if (controller.HasPermission(Permissions.AdminUsersRead))
{
result.AddGetLink("admin/users", controller.Url<UserManagementController>(x => nameof(x.GetUsers)));
}
return result;
}
}
}

17
src/Squidex/Areas/Api/Controllers/Users/UsersController.cs

@ -56,6 +56,23 @@ namespace Squidex.Areas.Api.Controllers.Users
this.log = log; this.log = log;
} }
/// <summary>
/// Get the user resources.
/// </summary>
/// <returns>
/// 200 => User resources returned.
/// </returns>
[HttpGet]
[Route("user/resources/")]
[ProducesResponseType(typeof(ResourcesDto), 200)]
[ApiPermission]
public IActionResult GetUserResources()
{
var response = ResourcesDto.FromController(this);
return Ok(response);
}
/// <summary> /// <summary>
/// Get users by query. /// Get users by query.
/// </summary> /// </summary>

8
src/Squidex/app/features/administration/administration-area.component.html

@ -1,18 +1,18 @@
<sqx-title message="Administration"></sqx-title> <sqx-title message="Administration"></sqx-title>
<div class="sidebar"> <div class="sidebar" *ngIf="uiState.resources | async; let resources">
<ul class="nav nav-panel flex-column"> <ul class="nav nav-panel flex-column">
<li class="nav-item" *sqxPermission="'squidex.admin.events.read'"> <li class="nav-item" *ngIf="resources | sqxHasLink:'admin/events'">
<a class="nav-link" routerLink="event-consumers" routerLinkActive="active"> <a class="nav-link" routerLink="event-consumers" routerLinkActive="active">
<i class="nav-icon icon-time"></i> <div class="nav-text">Consumers</div> <i class="nav-icon icon-time"></i> <div class="nav-text">Consumers</div>
</a> </a>
</li> </li>
<li class="nav-item" *sqxPermission="'squidex.admin.users.read'"> <li class="nav-item" *ngIf="resources | sqxHasLink:'admin/users'">
<a class="nav-link" routerLink="users" routerLinkActive="active"> <a class="nav-link" routerLink="users" routerLinkActive="active">
<i class="nav-icon icon-user-o"></i> <div class="nav-text">Users</div> <i class="nav-icon icon-user-o"></i> <div class="nav-text">Users</div>
</a> </a>
</li> </li>
<li class="nav-item" *sqxPermission="'squidex.admin.restore.read'"> <li class="nav-item" *ngIf="resources | sqxHasLink:'admin/restore'">
<a class="nav-link" routerLink="restore" routerLinkActive="active"> <a class="nav-link" routerLink="restore" routerLinkActive="active">
<i class="nav-icon icon-backup"></i> <div class="nav-text">Restore</div> <i class="nav-icon icon-backup"></i> <div class="nav-text">Restore</div>
</a> </a>

6
src/Squidex/app/features/administration/administration-area.component.ts

@ -7,10 +7,16 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { UIState } from '@app/shared';
@Component({ @Component({
selector: 'sqx-administration-area', selector: 'sqx-administration-area',
styleUrls: ['./administration-area.component.scss'], styleUrls: ['./administration-area.component.scss'],
templateUrl: './administration-area.component.html' templateUrl: './administration-area.component.html'
}) })
export class AdministrationAreaComponent { export class AdministrationAreaComponent {
constructor(
public readonly uiState: UIState
) {
}
} }

2
src/Squidex/app/features/dashboard/pages/dashboard-page.component.html

@ -12,7 +12,7 @@
</div> </div>
<div class="clearfix"> <div class="clearfix">
<a class="card card-href" [routerLink]="['schemas', { showDialog: true }]" *sqxPermission="'squidex.apps.{app}.schemas.*.read'"> <a class="card card-href" [routerLink]="['schemas', { showDialog: true }]" *ngIf="app | sqxHasLink:'schemas/create'">
<div class="card-body"> <div class="card-body">
<div class="card-image"> <div class="card-image">
<img src="./images/dashboard-schema.png" /> <img src="./images/dashboard-schema.png" />

5
src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts

@ -6,10 +6,9 @@
*/ */
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { filter, map, switchMap } from 'rxjs/operators'; import { switchMap } from 'rxjs/operators';
import { import {
AppDto,
AppsState, AppsState,
AuthService, AuthService,
DateTime, DateTime,
@ -52,7 +51,7 @@ export class DashboardPageComponent extends ResourceOwner implements OnInit {
public isPerformanceStacked = false; public isPerformanceStacked = false;
public app = this.appsState.selectedApp.pipe(filter(x => !!x), map(x => <AppDto>x)); public app = this.appsState.selectedValidApp;
public chartOptions = { public chartOptions = {
responsive: true, responsive: true,

18
src/Squidex/app/features/settings/settings-area.component.html

@ -6,50 +6,50 @@
</ng-container> </ng-container>
<ng-container content> <ng-container content>
<ul class="nav nav-panel nav-dark flex-column"> <ul class="nav nav-panel nav-dark flex-column" *ngIf="appsState.selectedValidApp | async; let selectedApp">
<li class="nav-item" *sqxPermission="'squidex.apps.{app}.backups.read'"> <li class="nav-item" *ngIf="selectedApp | sqxHasLink:'backups'">
<a class="nav-link" routerLink="backups" routerLinkActive="active"> <a class="nav-link" routerLink="backups" routerLinkActive="active">
Backups Backups
<i class="icon-angle-right"></i> <i class="icon-angle-right"></i>
</a> </a>
</li> </li>
<li class="nav-item" *sqxPermission="'squidex.apps.{app}.clients.read'"> <li class="nav-item" *ngIf="selectedApp | sqxHasLink:'clients'">
<a class="nav-link" routerLink="clients" routerLinkActive="active"> <a class="nav-link" routerLink="clients" routerLinkActive="active">
Clients Clients
<i class="icon-angle-right"></i> <i class="icon-angle-right"></i>
</a> </a>
</li> </li>
<li class="nav-item" *sqxPermission="'squidex.apps.{app}.contributors.read'"> <li class="nav-item" *ngIf="selectedApp | sqxHasLink:'contributors'">
<a class="nav-link" routerLink="contributors" routerLinkActive="active"> <a class="nav-link" routerLink="contributors" routerLinkActive="active">
Contributors Contributors
<i class="icon-angle-right"></i> <i class="icon-angle-right"></i>
</a> </a>
</li> </li>
<li class="nav-item" *sqxPermission="'squidex.apps.{app}.roles.read'"> <li class="nav-item" *ngIf="selectedApp | sqxHasLink:'roles'">
<a class="nav-link" routerLink="roles" routerLinkActive="active"> <a class="nav-link" routerLink="roles" routerLinkActive="active">
Roles Roles
<i class="icon-angle-right"></i> <i class="icon-angle-right"></i>
</a> </a>
</li> </li>
<li class="nav-item" *sqxPermission="'squidex.apps.{app}.languages.read'"> <li class="nav-item" *ngIf="selectedApp | sqxHasLink:'languages'">
<a class="nav-link" routerLink="languages" routerLinkActive="active"> <a class="nav-link" routerLink="languages" routerLinkActive="active">
Languages Languages
<i class="icon-angle-right"></i> <i class="icon-angle-right"></i>
</a> </a>
</li> </li>
<li class="nav-item" *sqxPermission="'squidex.apps.{app}.patterns.read'"> <li class="nav-item" *ngIf="selectedApp | sqxHasLink:'patterns'">
<a class="nav-link" routerLink="patterns" routerLinkActive="active"> <a class="nav-link" routerLink="patterns" routerLinkActive="active">
Patterns Patterns
<i class="icon-angle-right"></i> <i class="icon-angle-right"></i>
</a> </a>
</li> </li>
<li class="nav-item" *sqxPermission="'squidex.apps.{app}.plans.read'"> <li class="nav-item" *ngIf="selectedApp | sqxHasLink:'plans'">
<a class="nav-link" routerLink="plans" routerLinkActive="active"> <a class="nav-link" routerLink="plans" routerLinkActive="active">
Subscription Subscription
<i class="icon-angle-right"></i> <i class="icon-angle-right"></i>
</a> </a>
</li> </li>
<li class="nav-item" *sqxPermission="'squidex.apps.{app}.delete'"> <li class="nav-item" *ngIf="selectedApp | sqxHasLink:'delete'">
<a class="nav-link" routerLink="more" routerLinkActive="active"> <a class="nav-link" routerLink="more" routerLinkActive="active">
More More
<i class="icon-angle-right"></i> <i class="icon-angle-right"></i>

20
src/Squidex/app/framework/angular/http/hateos.pipes.ts

@ -18,17 +18,13 @@ import {
pure: true pure: true
}) })
export class HasLinkPipe implements PipeTransform { export class HasLinkPipe implements PipeTransform {
public transform(value: Resource | ResourceLinks, rel: string) { public transform(value: Resource | ResourceLinks, ...rels: string[]) {
return hasLink(value, rel); for (let rel of rels) {
} if (hasLink(value, rel)) {
} return true;
}
}
@Pipe({ return false;
name: 'sqxHasNoLink',
pure: true
})
export class HasNoLinkPipe implements PipeTransform {
public transform(value: Resource | ResourceLinks, rel: string) {
return !hasLink(value, rel);
} }
} }

1
src/Squidex/app/framework/internal.ts

@ -30,7 +30,6 @@ export * from './utils/math-helper';
export * from './utils/modal-positioner'; export * from './utils/modal-positioner';
export * from './utils/modal-view'; export * from './utils/modal-view';
export * from './utils/pager'; export * from './utils/pager';
export * from './utils/permission';
export * from './utils/rxjs-extensions'; export * from './utils/rxjs-extensions';
export * from './utils/string-helper'; export * from './utils/string-helper';
export * from './utils/types'; export * from './utils/types';

3
src/Squidex/app/framework/module.ts

@ -43,7 +43,6 @@ import {
FromNowPipe, FromNowPipe,
FullDateTimePipe, FullDateTimePipe,
HasLinkPipe, HasLinkPipe,
HasNoLinkPipe,
HoverBackgroundDirective, HoverBackgroundDirective,
IFrameEditorComponent, IFrameEditorComponent,
IgnoreScrollbarDirective, IgnoreScrollbarDirective,
@ -125,7 +124,6 @@ import {
FromNowPipe, FromNowPipe,
FullDateTimePipe, FullDateTimePipe,
HasLinkPipe, HasLinkPipe,
HasNoLinkPipe,
HoverBackgroundDirective, HoverBackgroundDirective,
IFrameEditorComponent, IFrameEditorComponent,
IgnoreScrollbarDirective, IgnoreScrollbarDirective,
@ -193,7 +191,6 @@ import {
FromNowPipe, FromNowPipe,
FullDateTimePipe, FullDateTimePipe,
HasLinkPipe, HasLinkPipe,
HasNoLinkPipe,
HoverBackgroundDirective, HoverBackgroundDirective,
IFrameEditorComponent, IFrameEditorComponent,
IgnoreScrollbarDirective, IgnoreScrollbarDirective,

145
src/Squidex/app/framework/utils/permission.spec.ts

@ -1,145 +0,0 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Permission } from './permission';
describe('Permission', () => {
it('should check when permissions are not equal', () => {
const g = new Permission('app.contents');
const r = new Permission('app.assets');
expect(g.allows(r)).toBeFalsy();
expect(g.includes(r)).toBeFalsy();
});
it('should check when permissions are equal with wildcards', () => {
const g = new Permission('app.*');
const r = new Permission('app.*');
expect(g.allows(r)).toBeTruthy();
expect(g.includes(r)).toBeTruthy();
});
it('should check when equal permissions', () => {
const g = new Permission('app.contents');
const r = new Permission('app.contents');
expect(g.allows(r)).toBeTruthy();
expect(g.includes(r)).toBeTruthy();
});
it('should check when given is parent of requested', () => {
const g = new Permission('app');
const r = new Permission('app.contents');
expect(g.allows(r)).toBeTruthy();
expect(g.includes(r)).toBeTruthy();
});
it('should check when requested is parent of given', () => {
const g = new Permission('app.contents');
const r = new Permission('app');
expect(g.allows(r)).toBeFalsy();
expect(g.includes(r)).toBeTruthy();
});
it('should check when given is wildcard of requested', () => {
const g = new Permission('app.*');
const r = new Permission('app.contents');
expect(g.allows(r)).toBeTruthy();
expect(g.includes(r)).toBeTruthy();
});
it('should check when requested is wildcard of given', () => {
const g = new Permission('app.contents');
const r = new Permission('app.*');
expect(g.allows(r)).toBeFalsy();
expect(g.includes(r)).toBeTruthy();
});
it('should check when given is has alternatives of requested', () => {
const g = new Permission('app.contents|schemas');
const r = new Permission('app.contents');
expect(g.allows(r)).toBeTruthy();
expect(g.includes(r)).toBeTruthy();
});
it('should check when requested is has alternatives of given', () => {
const g = new Permission('app.contents');
const r = new Permission('app.contents|schemas');
expect(g.allows(r)).toBeTruthy();
expect(g.includes(r)).toBeTruthy();
});
it('should check for requested is null', () => {
const g = new Permission('app.contents');
expect(g.allows(null!)).toBeFalsy();
expect(g.includes(null!)).toBeFalsy();
});
it('should return true if any permission gives permission to requested', () => {
const set = [
new Permission('app.contents'),
new Permission('app.assets')
];
expect(new Permission('app.contents').allowedBy(set)).toBeTruthy();
});
it('should return true if any permission includes parent given', () => {
const set = [
new Permission('app.contents'),
new Permission('app.assets')
];
expect(new Permission('app').includedIn(set)).toBeTruthy();
});
it('should return true if any permission includes child given', () => {
const set = [
new Permission('app.contents'),
new Permission('app.assets')
];
expect(new Permission('app.contents.read').includedIn(set)).toBeTruthy();
});
it('should return false if none permission gives permission to requested', () => {
const set = [
new Permission('app.contents'),
new Permission('app.assets')
];
expect(new Permission('app.schemas').allowedBy(set)).toBeFalsy();
});
it('should return false if none permission includes given', () => {
const set = [
new Permission('app.contents'),
new Permission('app.assets')
];
expect(new Permission('other').allowedBy(set)).toBeFalsy();
});
});

116
src/Squidex/app/framework/utils/permission.ts

@ -1,116 +0,0 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Types } from './types';
export class Permission {
private readonly parts: ({ [key: string]: true } | null)[];
constructor(
public readonly id: string
) {
this.parts = id.split('.').map(x => {
if (x === '*') {
return null;
} else {
const result: { [key: string]: true } = {};
for (let p of x.split('|')) {
result[p] = true;
}
return result;
}
});
}
public includedIn(permissions: Permission[]) {
for (let permission of permissions) {
if (permission.includes(this)) {
return true;
}
}
return false;
}
public allowedBy(permissions: Permission[]) {
for (let permission of permissions) {
if (permission.allows(this)) {
return true;
}
}
return false;
}
public includes(permission?: Permission | string) {
if (!permission) {
return false;
}
if (Types.isString(permission)) {
permission = new Permission(permission);
}
for (let i = 0; i < Math.min(permission.parts.length, this.parts.length); i++) {
const lhs = this.parts[i];
const rhs = permission.parts[i];
if (lhs != null && rhs != null && !Permission.intersects(lhs, rhs)) {
return false;
}
}
return true;
}
public allows(permission?: Permission | string) {
if (!permission) {
return false;
}
if (Types.isString(permission)) {
permission = new Permission(permission);
}
if (this.parts.length > permission.parts.length) {
return false;
}
for (let i = 0; i < this.parts.length; i++) {
const lhs = this.parts[i];
const rhs = permission.parts[i];
if (lhs !== null && (rhs === null || !Permission.intersects(lhs, rhs))) {
return false;
}
}
return true;
}
private static intersects(lhs: { [key: string]: true }, rhs: { [key: string]: true }) {
for (let key in lhs) {
if (lhs.hasOwnProperty(key)) {
if (rhs[key]) {
return true;
}
}
}
for (let key in rhs) {
if (rhs.hasOwnProperty(key)) {
if (lhs[key]) {
return true;
}
}
}
return false;
}
}

2
src/Squidex/app/shared/components/asset-uploader.component.html

@ -1,4 +1,4 @@
<ng-container *ngIf="appsState.selectedApp | async"> <ng-container *ngIf="appsState.selectedValidApp | async">
<ul class="nav navbar-nav" *ngIf="assetUploader.uploads | async; let uploads" (sqxFileDrop)="addFiles($event)"> <ul class="nav navbar-nav" *ngIf="assetUploader.uploads | async; let uploads" (sqxFileDrop)="addFiles($event)">
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<span class="nav-link dropdown-toggle" (click)="modalMenu.toggle()"> <span class="nav-link dropdown-toggle" (click)="modalMenu.toggle()">

133
src/Squidex/app/shared/components/permission.directive.ts

@ -1,133 +0,0 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectorRef, Directive, Input, OnChanges, OnInit, TemplateRef, ViewContainerRef } from '@angular/core';
import {
AppDto,
AppsState,
AuthService,
Permission,
ResourceOwner,
SchemaDto,
SchemasState
} from '@app/shared/internal';
@Directive({
selector: '[sqxPermission]'
})
export class PermissionDirective extends ResourceOwner implements OnChanges, OnInit {
private viewCreated = false;
@Input('sqxPermissionApp')
public app?: AppDto;
@Input('sqxPermissionSchema')
public schema?: SchemaDto;
@Input('sqxPermission')
public permissions: string;
constructor(
private readonly authService: AuthService,
private readonly appsState: AppsState,
private readonly changeDetector: ChangeDetectorRef,
private readonly schemasState: SchemasState,
private readonly templateRef: TemplateRef<any>,
private readonly viewContainer: ViewContainerRef
) {
super();
}
public ngOnInit() {
this.own(
this.appsState.selectedApp.subscribe(app => {
if (app && !this.app) {
this.update(app, this.schemasState.snapshot.selectedSchema);
}
}));
this.own(
this.schemasState.selectedSchema.subscribe(schema => {
if (schema && !this.schema) {
this.update(this.appsState.snapshot.selectedApp, schema);
}
}));
}
public ngOnChanges() {
this.update(this.appsState.snapshot.selectedApp, this.schemasState.snapshot.selectedSchema);
}
private update(app?: AppDto | null, schema?: SchemaDto | null) {
if (this.app) {
app = this.app;
}
if (this.schema) {
schema = this.schema;
}
let permissions = this.permissions;
let show = false;
if (permissions) {
let include = permissions[0] === '?';
if (include) {
permissions = permissions.substr(1);
}
const array = permissions.split(';');
for (let id of array) {
if (app) {
id = id.replace('{app}', app.name);
}
if (schema) {
id = id.replace('{name}', schema.name);
}
const permission = new Permission(id);
if (include) {
if (app && permission.includedIn(app.permissions)) {
show = true;
}
if (!show) {
show = permission.includedIn(this.authService.user!.permissions);
}
} else {
if (app && permission.allowedBy(app.permissions)) {
show = true;
}
if (!show) {
show = permission.allowedBy(this.authService.user!.permissions);
}
}
if (show) {
break;
}
}
}
if (show && !this.viewCreated) {
this.viewContainer.createEmbeddedView(this.templateRef);
this.viewCreated = true;
} else if (!show && this.viewCreated) {
this.viewContainer.clear();
this.viewCreated = false;
}
this.changeDetector.markForCheck();
}
}

2
src/Squidex/app/shared/components/schema-category.component.html

@ -15,7 +15,7 @@
<ul class="nav nav-panel nav-dark nav-dark-bordered flex-column" *ngIf="snapshot.isOpen" @fade> <ul class="nav nav-panel nav-dark nav-dark-bordered flex-column" *ngIf="snapshot.isOpen" @fade>
<ng-container *ngFor="let schema of snapshot.schemasFiltered; trackBy: trackBySchema"> <ng-container *ngFor="let schema of snapshot.schemasFiltered; trackBy: trackBySchema">
<ng-container *sqxPermission="schemaPermission(schema)"> <ng-container *ngIf="schemaPermission(schema)">
<li class="nav-item" dnd-draggable [dragEnabled]="!isReadonly" [dragData]="schema"> <li class="nav-item" dnd-draggable [dragEnabled]="!isReadonly" [dragData]="schema">
<a class="nav-link" [routerLink]="schemaRoute(schema)" routerLinkActive="active"> <a class="nav-link" [routerLink]="schemaRoute(schema)" routerLinkActive="active">
<div class="row" *ngIf="!isReadonly"> <div class="row" *ngIf="!isReadonly">

1
src/Squidex/app/shared/declarations.ts

@ -20,7 +20,6 @@ export * from './components/history.component';
export * from './components/history-list.component'; export * from './components/history-list.component';
export * from './components/language-selector.component'; export * from './components/language-selector.component';
export * from './components/markdown-editor.component'; export * from './components/markdown-editor.component';
export * from './components/permission.directive';
export * from './components/pipes'; export * from './components/pipes';
export * from './components/rich-editor.component'; export * from './components/rich-editor.component';
export * from './components/schema-category.component'; export * from './components/schema-category.component';

3
src/Squidex/app/shared/module.ts

@ -64,7 +64,6 @@ import {
NewsService, NewsService,
PatternsService, PatternsService,
PatternsState, PatternsState,
PermissionDirective,
PlansService, PlansService,
PlansState, PlansState,
RichEditorComponent, RichEditorComponent,
@ -123,7 +122,6 @@ import {
HistoryMessagePipe, HistoryMessagePipe,
LanguageSelectorComponent, LanguageSelectorComponent,
MarkdownEditorComponent, MarkdownEditorComponent,
PermissionDirective,
SchemaCategoryComponent, SchemaCategoryComponent,
UserDtoPicture, UserDtoPicture,
UserIdPicturePipe, UserIdPicturePipe,
@ -155,7 +153,6 @@ import {
HistoryMessagePipe, HistoryMessagePipe,
LanguageSelectorComponent, LanguageSelectorComponent,
MarkdownEditorComponent, MarkdownEditorComponent,
PermissionDirective,
RichEditorComponent, RichEditorComponent,
RouterModule, RouterModule,
SchemaCategoryComponent, SchemaCategoryComponent,

7
src/Squidex/app/shared/services/apps.service.spec.ts

@ -14,8 +14,7 @@ import {
AppCreatedDto, AppCreatedDto,
AppDto, AppDto,
AppsService, AppsService,
DateTime, DateTime
Permission
} from '@app/shared/internal'; } from '@app/shared/internal';
describe('AppsService', () => { describe('AppsService', () => {
@ -73,8 +72,8 @@ describe('AppsService', () => {
expect(apps!).toEqual( expect(apps!).toEqual(
[ [
new AppDto('123', 'name1', [new Permission('Owner')], DateTime.parseISO('2016-01-01'), DateTime.parseISO('2016-02-02'), 'Free', 'Basic'), new AppDto('123', 'name1', ['Owner'], DateTime.parseISO('2016-01-01'), DateTime.parseISO('2016-02-02'), 'Free', 'Basic'),
new AppDto('456', 'name2', [new Permission('Owner')], DateTime.parseISO('2017-01-01'), DateTime.parseISO('2017-02-02'), 'Basic', 'Enterprise') new AppDto('456', 'name2', ['Owner'], DateTime.parseISO('2017-01-01'), DateTime.parseISO('2017-02-02'), 'Basic', 'Enterprise')
]); ]);
})); }));

37
src/Squidex/app/shared/services/apps.service.ts

@ -15,15 +15,18 @@ import {
ApiUrlConfig, ApiUrlConfig,
DateTime, DateTime,
Model, Model,
Permission, pretifyError,
pretifyError ResourceLinks,
withLinks
} from '@app/framework'; } from '@app/framework';
export class AppDto extends Model<AppDto> { export class AppDto extends Model<AppDto> {
public readonly _links: ResourceLinks = {};
constructor( constructor(
public readonly id: string, public readonly id: string,
public readonly name: string, public readonly name: string,
public readonly permissions: Permission[], public readonly permissions: string[],
public readonly created: DateTime, public readonly created: DateTime,
public readonly lastModified: DateTime, public readonly lastModified: DateTime,
public readonly planName?: string, public readonly planName?: string,
@ -59,18 +62,7 @@ export class AppsService {
return this.http.get<any[]>(url).pipe( return this.http.get<any[]>(url).pipe(
map(body => { map(body => {
const apps = body.map(item => { const apps = body.map(item => parseApp(item));
const permissions = (<string[]>item.permissions).map(x => new Permission(x));
return new AppDto(
item.id,
item.name,
permissions,
DateTime.parseISO(item.created),
DateTime.parseISO(item.lastModified),
item.planName,
item.planUpgrade);
});
return apps; return apps;
}), }),
@ -96,4 +88,17 @@ export class AppsService {
}), }),
pretifyError('Failed to archive app. Please reload.')); pretifyError('Failed to archive app. Please reload.'));
} }
} }
function parseApp(response: any) {
return withLinks(
new AppDto(
response.id,
response.name,
response.permissions,
DateTime.parseISO(response.created),
DateTime.parseISO(response.lastModified),
response.planName,
response.planUpgrade),
response);
}

21
src/Squidex/app/shared/services/auth.service.ts

@ -16,15 +16,9 @@ import {
WebStorageStateStore WebStorageStateStore
} from 'oidc-client'; } from 'oidc-client';
import { import { ApiUrlConfig, Types } from '@app/framework';
ApiUrlConfig,
Permission,
Types
} from '@app/framework';
export class Profile { export class Profile {
public readonly permissions: Permission[];
public get id(): string { public get id(): string {
return this.user.profile['sub']; return this.user.profile['sub'];
} }
@ -56,19 +50,6 @@ export class Profile {
constructor( constructor(
public readonly user: User public readonly user: User
) { ) {
const permissions = this.user.profile['urn:squidex:permissions'];
if (Types.isArrayOfString(permissions)) {
this.permissions = permissions.map(x => new Permission(x));
} else if (Types.isString(permissions)) {
this.permissions = [new Permission(permissions)];
} else {
this.permissions = [];
}
if (this.user.profile['role'] === 'ADMINISTRATOR') {
this.permissions.push( new Permission('squidex.admin'));
}
} }
} }

28
src/Squidex/app/shared/services/users.service.spec.ts

@ -13,6 +13,7 @@ import {
UserDto, UserDto,
UsersService UsersService
} from '@app/shared/internal'; } from '@app/shared/internal';
import { ResourcesDto } from './users.service';
describe('UsersService', () => { describe('UsersService', () => {
beforeEach(() => { beforeEach(() => {
@ -113,4 +114,31 @@ describe('UsersService', () => {
expect(user!).toEqual(new UserDto('123', 'User1')); expect(user!).toEqual(new UserDto('123', 'User1'));
})); }));
it('should make get request to get resources',
inject([UsersService, HttpTestingController], (usersService: UsersService, httpMock: HttpTestingController) => {
let resources: ResourcesDto;
usersService.getResources().subscribe(result => {
resources = result;
});
const req = httpMock.expectOne('http://service/p/api/user/resources');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({
_links: {
schemas: { method: 'GET', href: '/api/schemas' }
}
});
const expected = new ResourcesDto();
expected._links['schemas'] = { method: 'GET', href: '/api/schemas' };
expect(resources!).toEqual(expected);
}));
}); });

21
src/Squidex/app/shared/services/users.service.ts

@ -10,7 +10,12 @@ import { Injectable } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { ApiUrlConfig, pretifyError } from '@app/framework'; import {
ApiUrlConfig,
pretifyError,
ResourceLinks,
withLinks
} from '@app/framework';
export class UserDto { export class UserDto {
constructor( constructor(
@ -20,6 +25,10 @@ export class UserDto {
} }
} }
export class ResourcesDto {
public readonly _links: ResourceLinks = {};
}
@Injectable() @Injectable()
export class UsersService { export class UsersService {
constructor( constructor(
@ -56,4 +65,14 @@ export class UsersService {
}), }),
pretifyError('Failed to load user. Please reload.')); pretifyError('Failed to load user. Please reload.'));
} }
public getResources(): Observable<ResourcesDto> {
const url = this.apiUrl.buildUrl(`api/user/resources`);
return this.http.get<any>(url).pipe(
map(body => {
return withLinks(new ResourcesDto(), body);
}),
pretifyError('Failed to load user. Please reload.'));
}
} }

9
src/Squidex/app/shared/state/apps.state.spec.ts

@ -13,19 +13,18 @@ import {
AppsService, AppsService,
AppsState, AppsState,
DateTime, DateTime,
DialogService, DialogService
Permission
} from '@app/shared/internal'; } from '@app/shared/internal';
describe('AppsState', () => { describe('AppsState', () => {
const now = DateTime.now(); const now = DateTime.now();
const oldApps = [ const oldApps = [
new AppDto('id1', 'old-name1', [new Permission('Owner')], now, now), new AppDto('id1', 'old-name1', ['Owner'], now, now),
new AppDto('id2', 'old-name2', [new Permission('Owner')], now, now) new AppDto('id2', 'old-name2', ['Owner'], now, now)
]; ];
const newApp = new AppDto('id3', 'new-name', [new Permission('Owner')], now, now); const newApp = new AppDto('id3', 'new-name', ['Owner'], now, now);
let dialogs: IMock<DialogService>; let dialogs: IMock<DialogService>;
let appsService: IMock<AppsService>; let appsService: IMock<AppsService>;

9
src/Squidex/app/shared/state/apps.state.ts

@ -7,13 +7,12 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { distinctUntilChanged, map, tap } from 'rxjs/operators'; import { distinctUntilChanged, filter, map, tap } from 'rxjs/operators';
import { import {
DateTime, DateTime,
DialogService, DialogService,
ImmutableArray, ImmutableArray,
Permission,
shareSubscribed, shareSubscribed,
State State
} from '@app/framework'; } from '@app/framework';
@ -47,6 +46,10 @@ export class AppsState extends State<Snapshot> {
this.changes.pipe(map(s => s.selectedApp), this.changes.pipe(map(s => s.selectedApp),
distinctUntilChanged(sameApp)); distinctUntilChanged(sameApp));
public selectedValidApp =
this.selectedApp.pipe(filter(x => !!x), map(x => <AppDto>x),
distinctUntilChanged());
public apps = public apps =
this.changes.pipe(map(s => s.apps), this.changes.pipe(map(s => s.apps),
distinctUntilChanged()); distinctUntilChanged());
@ -116,7 +119,7 @@ function createApp(request: CreateAppDto, response: AppCreatedDto, now?: DateTim
const app = new AppDto( const app = new AppDto(
response.id, response.id,
request.name, request.name,
response.permissions.map(x => new Permission(x)), response.permissions,
now, now,
now, now,
response.planName, response.planName,

23
src/Squidex/app/shared/state/ui.state.spec.ts

@ -8,7 +8,14 @@
import { of } from 'rxjs'; import { of } from 'rxjs';
import { IMock, It, Mock, Times } from 'typemoq'; import { IMock, It, Mock, Times } from 'typemoq';
import { UIService, UIState } from '@app/shared/internal'; import {
ResourceLinks,
ResourcesDto,
UIService,
UIState,
UsersService,
withLinks
} from '@app/shared/internal';
import { TestValues } from './_test-helpers'; import { TestValues } from './_test-helpers';
@ -30,6 +37,11 @@ describe('UIState', () => {
canCreateApps: true canCreateApps: true
}; };
const resources: ResourceLinks = {
schemas: { method: 'GET', href: '/api/schemas' }
};
let usersService: IMock<UsersService>;
let uiService: IMock<UIService>; let uiService: IMock<UIService>;
let uiState: UIState; let uiState: UIState;
@ -48,7 +60,12 @@ describe('UIState', () => {
uiService.setup(x => x.deleteSetting(app, It.isAnyString())) uiService.setup(x => x.deleteSetting(app, It.isAnyString()))
.returns(() => of({})); .returns(() => of({}));
uiState = new UIState(appsState.object, uiService.object); usersService = Mock.ofType<UsersService>();
usersService.setup(x => x.getResources())
.returns(() => of(withLinks(new ResourcesDto(), { _links: resources })));
uiState = new UIState(appsState.object, uiService.object, usersService.object);
}); });
it('should load settings', () => { it('should load settings', () => {
@ -58,6 +75,8 @@ describe('UIState', () => {
mapSize: 1024, mapSize: 1024,
canCreateApps: true canCreateApps: true
}); });
expect(uiState.snapshot.resources).toEqual(resources);
}); });
it('should add value to snapshot when set', () => { it('should add value to snapshot when set', () => {

37
src/Squidex/app/shared/state/ui.state.ts

@ -8,12 +8,18 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { distinctUntilChanged, map } from 'rxjs/operators'; import { distinctUntilChanged, map } from 'rxjs/operators';
import { State, Types } from '@app/framework'; import {
ResourceLinks,
State,
Types
} from '@app/framework';
import { AppsState } from './apps.state'; import { AppsState } from './apps.state';
import { UIService, UISettingsDto } from './../services/ui.service'; import { UIService, UISettingsDto } from './../services/ui.service';
import { UsersService } from './../services/users.service';
interface Snapshot { interface Snapshot {
// All common settings. // All common settings.
settingsCommon: object & any; settingsCommon: object & any;
@ -23,6 +29,8 @@ interface Snapshot {
// The merged settings of app and common settings. // The merged settings of app and common settings.
settings: object & any; settings: object & any;
resources: ResourceLinks;
} }
@Injectable() @Injectable()
@ -31,6 +39,10 @@ export class UIState extends State<Snapshot> {
this.changes.pipe(map(x => x.settings), this.changes.pipe(map(x => x.settings),
distinctUntilChanged()); distinctUntilChanged());
public resources =
this.changes.pipe(map(x => x.resources),
distinctUntilChanged());
public get<T>(path: string, defaultValue: T) { public get<T>(path: string, defaultValue: T) {
return this.settings.pipe(map(x => this.getValue(x, path, defaultValue)), return this.settings.pipe(map(x => this.getValue(x, path, defaultValue)),
distinctUntilChanged()); distinctUntilChanged());
@ -38,16 +50,16 @@ export class UIState extends State<Snapshot> {
constructor( constructor(
private readonly appsState: AppsState, private readonly appsState: AppsState,
private readonly uiService: UIService private readonly uiService: UIService,
private readonly usersService: UsersService
) { ) {
super({ settings: { }, settingsCommon: { }, settingsApp: { } }); super({ settings: {}, settingsCommon: {}, settingsApp: {}, resources: {} });
this.loadResources();
this.loadCommon(); this.loadCommon();
appsState.selectedApp.subscribe(app => { appsState.selectedValidApp.subscribe(app => {
if (app) { this.load();
this.load();
}
}); });
} }
@ -67,6 +79,13 @@ export class UIState extends State<Snapshot> {
}); });
} }
private loadResources() {
this.usersService.getResources()
.subscribe(payload => {
this.next(s => ({ ...s, resources: payload._links }));
});
}
public set(path: string, value: any) { public set(path: string, value: any) {
const { key, current, root } = this.getContainer(path); const { key, current, root } = this.getContainer(path);
@ -151,11 +170,11 @@ export class UIState extends State<Snapshot> {
function updateAppSettings(state: Snapshot, settingsApp: object & any) { function updateAppSettings(state: Snapshot, settingsApp: object & any) {
const { settingsCommon } = state; const { settingsCommon } = state;
return { settings: { ...settingsCommon, ...settingsApp }, settingsApp, settingsCommon }; return { ...state, settings: { ...settingsCommon, ...settingsApp }, settingsApp, settingsCommon };
} }
function updateCommonSettings(state: Snapshot, settingsCommon: object & any) { function updateCommonSettings(state: Snapshot, settingsCommon: object & any) {
const { settingsApp } = state; const { settingsApp } = state;
return { settings: { ...settingsCommon, ...settingsApp }, settingsApp, settingsCommon }; return { ...state, settings: { ...settingsCommon, ...settingsApp }, settingsApp, settingsCommon };
} }

14
src/Squidex/app/shell/pages/app/left-menu.component.html

@ -1,30 +1,30 @@
<ul class="nav flex-column"> <ul class="nav flex-column" *ngIf="appsState.selectedValidApp | async; let selectedApp">
<li class="nav-item" *sqxPermission="'squidex.apps.{app}.schemas.*.read'"> <li class="nav-item" *ngIf="selectedApp | sqxHasLink:'schemas'">
<a class="nav-link" routerLink="schemas" routerLinkActive="active"> <a class="nav-link" routerLink="schemas" routerLinkActive="active">
<i class="nav-icon icon-schemas"></i> <div class="nav-text">Schemas</div> <i class="nav-icon icon-schemas"></i> <div class="nav-text">Schemas</div>
</a> </a>
</li> </li>
<li class="nav-item" *sqxPermission="'?squidex.apps.{app}.contents.*.read'"> <li class="nav-item" *ngIf="selectedApp | sqxHasLink:'contents'">
<a class="nav-link" routerLink="content" routerLinkActive="active"> <a class="nav-link" routerLink="content" routerLinkActive="active">
<i class="nav-icon icon-contents"></i> <div class="nav-text">Content</div> <i class="nav-icon icon-contents"></i> <div class="nav-text">Content</div>
</a> </a>
</li> </li>
<li class="nav-item" *sqxPermission="'squidex.apps.{app}.assets.read'"> <li class="nav-item" *ngIf="selectedApp | sqxHasLink:'assets'">
<a class="nav-link" routerLink="assets/filters" routerLinkActive="active"> <a class="nav-link" routerLink="assets/filters" routerLinkActive="active">
<i class="nav-icon icon-assets"></i> <div class="nav-text">Assets</div> <i class="nav-icon icon-assets"></i> <div class="nav-text">Assets</div>
</a> </a>
</li> </li>
<li class="nav-item" *sqxPermission="'squidex.apps.{app}.rules.read'"> <li class="nav-item" *ngIf="selectedApp | sqxHasLink:'rules'">
<a class="nav-link" routerLink="rules" routerLinkActive="active"> <a class="nav-link" routerLink="rules" routerLinkActive="active">
<i class="nav-icon icon-rules"></i> <div class="nav-text">Rules</div> <i class="nav-icon icon-rules"></i> <div class="nav-text">Rules</div>
</a> </a>
</li> </li>
<li class="nav-item" *sqxPermission="'?squidex.apps.{app}.backups|clients|contributors|languages|patterns|plans.read;squidex.apps.{app}.delete'"> <li class="nav-item">
<a class="nav-link" routerLink="settings" routerLinkActive="active"> <a class="nav-link" routerLink="settings" routerLinkActive="active">
<i class="nav-icon icon-settings"></i> <div class="nav-text">Settings</div> <i class="nav-icon icon-settings"></i> <div class="nav-text">Settings</div>
</a> </a>
</li> </li>
<li class="nav-item" *sqxPermission="'squidex.apps.{app}.api'"> <li class="nav-item" *ngIf="selectedApp | sqxHasLink:'api'">
<a class="nav-link" routerLink="api" routerLinkActive="active"> <a class="nav-link" routerLink="api" routerLinkActive="active">
<i class="nav-icon icon-api"></i> <div class="nav-text">API</div> <i class="nav-icon icon-api"></i> <div class="nav-text">API</div>
</a> </a>

3
src/Squidex/app/shell/pages/app/left-menu.component.ts

@ -16,7 +16,8 @@ import { AppsState } from '@app/shared';
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class LeftMenuComponent { export class LeftMenuComponent {
constructor(public readonly appsState: AppsState constructor(
public readonly appsState: AppsState
) { ) {
} }
} }

24
src/Squidex/app/shell/pages/internal/apps-menu.component.html

@ -38,19 +38,17 @@
</ng-container> </ng-container>
</li> </li>
<ng-container *ngIf="appsState.selectedApp | async; let selectedApp"> <ng-container *ngIf="appsState.selectedValidApp | async; let selectedApp">
<ng-container *ngIf="selectedApp.planUpgrade && selectedApp.planName"> <li class="nav-item" *ngIf="selectedApp.planUpgrade && selectedApp.planName">
<li class="nav-item" *sqxPermission="'squidex.apps.{app}.plans.change'"> <div class="btn-group app-upgrade">
<div class="btn-group app-upgrade"> <button type="button" class="btn btn-primary btn-plan">
<button type="button" class="btn btn-primary btn-plan"> You are on the <strong>{{selectedApp.planName}}</strong> plan.
You are on the <strong>{{selectedApp.planName}}</strong> plan. </button>
</button> <button type="button" class="btn btn-warning" [routerLink]="['/app', selectedApp.name, 'settings', 'plans']">
<button type="button" class="btn btn-warning" [routerLink]="['/app', selectedApp.name, 'settings', 'plans']"> Upgrade!
Upgrade! </button>
</button> </div>
</div> </li>
</li>
</ng-container>
</ng-container> </ng-container>
</ul> </ul>

8
src/Squidex/app/shell/pages/internal/profile-menu.component.html

@ -17,9 +17,11 @@
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item" routerLink="/app/administration" *sqxPermission="'?squidex.admin.events|restore|users.read'"> <ng-container *ngIf="uiState.resources | async; let resources">
Administration <a class="dropdown-item" routerLink="/app/administration" *ngIf="resources | sqxHasLink:'admin/events':'admin/restore':'admin/users'">
</a> Administration
</a>
</ng-container>
<a class="dropdown-item" [sqxPopupLink]="snapshot.profileUrl"> <a class="dropdown-item" [sqxPopupLink]="snapshot.profileUrl">
Profile Profile

4
src/Squidex/app/shell/pages/internal/profile-menu.component.ts

@ -12,7 +12,8 @@ import {
AuthService, AuthService,
fadeAnimation, fadeAnimation,
ModalModel, ModalModel,
StatefulComponent StatefulComponent,
UIState
} from '@app/shared'; } from '@app/shared';
interface State { interface State {
@ -35,6 +36,7 @@ export class ProfileMenuComponent extends StatefulComponent<State> implements On
public modalMenu = new ModalModel(); public modalMenu = new ModalModel();
constructor(changeDetector: ChangeDetectorRef, apiUrl: ApiUrlConfig, constructor(changeDetector: ChangeDetectorRef, apiUrl: ApiUrlConfig,
public readonly uiState: UIState,
private readonly authService: AuthService private readonly authService: AuthService
) { ) {
super(changeDetector, { super(changeDetector, {

Loading…
Cancel
Save