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 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";
@ -117,7 +116,6 @@ namespace Squidex.Shared
public const string AppContents = "squidex.apps.{app}.contents.{name}";
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 AppContentsUpdate = "squidex.apps.{app}.contents.{name}.update";
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>();
@ -38,22 +38,22 @@ namespace Squidex.Web
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 == "*")
{
@ -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]
[Route("apps/{app}/patterns/")]
[ProducesResponseType(typeof(AppPatternDto[]), 200)]
[ApiPermission(Permissions.AppPatternsRead)]
[ApiPermission(Permissions.AppCommon)]
[ApiCosts(0)]
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()
{
var userOrClientId = HttpContext.User.UserOrClientId();
var userPermissions = HttpContext.User.Permissions();
var userPermissions = HttpContext.Permissions();
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();

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

@ -9,6 +9,11 @@ using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
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.Services;
using Squidex.Infrastructure;
@ -16,10 +21,11 @@ using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.Security;
using Squidex.Shared;
using Squidex.Web;
using AllPermissions = Squidex.Shared.Permissions;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
public sealed class AppDto : IGenerateETag
public sealed class AppDto : Resource, IGenerateETag
{
/// <summary>
/// The name of the app.
@ -63,7 +69,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// </summary>
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>();
@ -77,13 +83,84 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
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);
response.PlanName = plans.GetPlanForApp(app)?.Name;
response.PlanUpgrade = plans.GetPlanUpgradeForApp(app)?.Name;
result.Permissions = permissions.ToArray(x => x.Id);
result.PlanName = plans.GetPlanForApp(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)]
[ApiPermission(Permissions.AppPlansChange)]
[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());

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

@ -5,49 +5,20 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using NodaTime;
using Squidex.Areas.Api.Controllers.Schemas.Models.Converters;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Reflection;
using Squidex.Web;
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>();
/// <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>
/// The scripts.
/// </summary>
@ -64,40 +35,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
[Required]
public List<FieldDto> Fields { get; set; }
/// <summary>
/// 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)
public static SchemaDetailsDto FromSchemaWithDetails(ISchemaEntity schema, ApiController controller, string app)
{
var response = new SchemaDetailsDto();
@ -147,7 +85,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
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.ComponentModel.DataAnnotations;
using NodaTime;
using Squidex.Areas.Api.Controllers.Contents;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Reflection;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Schemas.Models
{
public sealed class SchemaDto : IGenerateETag
public class SchemaDto : Resource, IGenerateETag
{
/// <summary>
/// The id of the schema.
@ -77,7 +79,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
/// </summary>
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() };
@ -85,6 +87,20 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
SimpleMapper.Map(schema.SchemaDef, response);
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;
}
}

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 response = schemas.ToArray(SchemaDto.FromSchema);
var response = schemas.ToArray(x => SchemaDto.FromSchema(x, this, app));
Response.Headers[HeaderNames.ETag] = response.ToManyEtag();
@ -90,7 +90,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
return NotFound();
}
var response = SchemaDetailsDto.FromSchema(entity);
var response = SchemaDetailsDto.FromSchemaWithDetails(entity, this, app);
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;
}
/// <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>
/// Get users by query.
/// </summary>

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

@ -1,18 +1,18 @@
<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">
<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">
<i class="nav-icon icon-time"></i> <div class="nav-text">Consumers</div>
</a>
</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">
<i class="nav-icon icon-user-o"></i> <div class="nav-text">Users</div>
</a>
</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">
<i class="nav-icon icon-backup"></i> <div class="nav-text">Restore</div>
</a>

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

@ -7,10 +7,16 @@
import { Component } from '@angular/core';
import { UIState } from '@app/shared';
@Component({
selector: 'sqx-administration-area',
styleUrls: ['./administration-area.component.scss'],
templateUrl: './administration-area.component.html'
})
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 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-image">
<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 { filter, map, switchMap } from 'rxjs/operators';
import { switchMap } from 'rxjs/operators';
import {
AppDto,
AppsState,
AuthService,
DateTime,
@ -52,7 +51,7 @@ export class DashboardPageComponent extends ResourceOwner implements OnInit {
public isPerformanceStacked = false;
public app = this.appsState.selectedApp.pipe(filter(x => !!x), map(x => <AppDto>x));
public app = this.appsState.selectedValidApp;
public chartOptions = {
responsive: true,

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

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

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

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

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-view';
export * from './utils/pager';
export * from './utils/permission';
export * from './utils/rxjs-extensions';
export * from './utils/string-helper';
export * from './utils/types';

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

@ -43,7 +43,6 @@ import {
FromNowPipe,
FullDateTimePipe,
HasLinkPipe,
HasNoLinkPipe,
HoverBackgroundDirective,
IFrameEditorComponent,
IgnoreScrollbarDirective,
@ -125,7 +124,6 @@ import {
FromNowPipe,
FullDateTimePipe,
HasLinkPipe,
HasNoLinkPipe,
HoverBackgroundDirective,
IFrameEditorComponent,
IgnoreScrollbarDirective,
@ -193,7 +191,6 @@ import {
FromNowPipe,
FullDateTimePipe,
HasLinkPipe,
HasNoLinkPipe,
HoverBackgroundDirective,
IFrameEditorComponent,
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)">
<li class="nav-item dropdown">
<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>
<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">
<a class="nav-link" [routerLink]="schemaRoute(schema)" routerLinkActive="active">
<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/language-selector.component';
export * from './components/markdown-editor.component';
export * from './components/permission.directive';
export * from './components/pipes';
export * from './components/rich-editor.component';
export * from './components/schema-category.component';

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

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

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

@ -14,8 +14,7 @@ import {
AppCreatedDto,
AppDto,
AppsService,
DateTime,
Permission
DateTime
} from '@app/shared/internal';
describe('AppsService', () => {
@ -73,8 +72,8 @@ describe('AppsService', () => {
expect(apps!).toEqual(
[
new AppDto('123', 'name1', [new Permission('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('123', 'name1', ['Owner'], DateTime.parseISO('2016-01-01'), DateTime.parseISO('2016-02-02'), 'Free', 'Basic'),
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,
DateTime,
Model,
Permission,
pretifyError
pretifyError,
ResourceLinks,
withLinks
} from '@app/framework';
export class AppDto extends Model<AppDto> {
public readonly _links: ResourceLinks = {};
constructor(
public readonly id: string,
public readonly name: string,
public readonly permissions: Permission[],
public readonly permissions: string[],
public readonly created: DateTime,
public readonly lastModified: DateTime,
public readonly planName?: string,
@ -59,18 +62,7 @@ export class AppsService {
return this.http.get<any[]>(url).pipe(
map(body => {
const apps = body.map(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);
});
const apps = body.map(item => parseApp(item));
return apps;
}),
@ -96,4 +88,17 @@ export class AppsService {
}),
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
} from 'oidc-client';
import {
ApiUrlConfig,
Permission,
Types
} from '@app/framework';
import { ApiUrlConfig, Types } from '@app/framework';
export class Profile {
public readonly permissions: Permission[];
public get id(): string {
return this.user.profile['sub'];
}
@ -56,19 +50,6 @@ export class Profile {
constructor(
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,
UsersService
} from '@app/shared/internal';
import { ResourcesDto } from './users.service';
describe('UsersService', () => {
beforeEach(() => {
@ -113,4 +114,31 @@ describe('UsersService', () => {
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 { map } from 'rxjs/operators';
import { ApiUrlConfig, pretifyError } from '@app/framework';
import {
ApiUrlConfig,
pretifyError,
ResourceLinks,
withLinks
} from '@app/framework';
export class UserDto {
constructor(
@ -20,6 +25,10 @@ export class UserDto {
}
}
export class ResourcesDto {
public readonly _links: ResourceLinks = {};
}
@Injectable()
export class UsersService {
constructor(
@ -56,4 +65,14 @@ export class UsersService {
}),
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,
AppsState,
DateTime,
DialogService,
Permission
DialogService
} from '@app/shared/internal';
describe('AppsState', () => {
const now = DateTime.now();
const oldApps = [
new AppDto('id1', 'old-name1', [new Permission('Owner')], now, now),
new AppDto('id2', 'old-name2', [new Permission('Owner')], now, now)
new AppDto('id1', 'old-name1', ['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 appsService: IMock<AppsService>;

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

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

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

@ -8,7 +8,14 @@
import { of } from 'rxjs';
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';
@ -30,6 +37,11 @@ describe('UIState', () => {
canCreateApps: true
};
const resources: ResourceLinks = {
schemas: { method: 'GET', href: '/api/schemas' }
};
let usersService: IMock<UsersService>;
let uiService: IMock<UIService>;
let uiState: UIState;
@ -48,7 +60,12 @@ describe('UIState', () => {
uiService.setup(x => x.deleteSetting(app, It.isAnyString()))
.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', () => {
@ -58,6 +75,8 @@ describe('UIState', () => {
mapSize: 1024,
canCreateApps: true
});
expect(uiState.snapshot.resources).toEqual(resources);
});
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 { distinctUntilChanged, map } from 'rxjs/operators';
import { State, Types } from '@app/framework';
import {
ResourceLinks,
State,
Types
} from '@app/framework';
import { AppsState } from './apps.state';
import { UIService, UISettingsDto } from './../services/ui.service';
import { UsersService } from './../services/users.service';
interface Snapshot {
// All common settings.
settingsCommon: object & any;
@ -23,6 +29,8 @@ interface Snapshot {
// The merged settings of app and common settings.
settings: object & any;
resources: ResourceLinks;
}
@Injectable()
@ -31,6 +39,10 @@ export class UIState extends State<Snapshot> {
this.changes.pipe(map(x => x.settings),
distinctUntilChanged());
public resources =
this.changes.pipe(map(x => x.resources),
distinctUntilChanged());
public get<T>(path: string, defaultValue: T) {
return this.settings.pipe(map(x => this.getValue(x, path, defaultValue)),
distinctUntilChanged());
@ -38,16 +50,16 @@ export class UIState extends State<Snapshot> {
constructor(
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();
appsState.selectedApp.subscribe(app => {
if (app) {
this.load();
}
appsState.selectedValidApp.subscribe(app => {
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) {
const { key, current, root } = this.getContainer(path);
@ -151,11 +170,11 @@ export class UIState extends State<Snapshot> {
function updateAppSettings(state: Snapshot, settingsApp: object & any) {
const { settingsCommon } = state;
return { settings: { ...settingsCommon, ...settingsApp }, settingsApp, settingsCommon };
return { ...state, settings: { ...settingsCommon, ...settingsApp }, settingsApp, settingsCommon };
}
function updateCommonSettings(state: Snapshot, settingsCommon: object & any) {
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">
<li class="nav-item" *sqxPermission="'squidex.apps.{app}.schemas.*.read'">
<ul class="nav flex-column" *ngIf="appsState.selectedValidApp | async; let selectedApp">
<li class="nav-item" *ngIf="selectedApp | sqxHasLink:'schemas'">
<a class="nav-link" routerLink="schemas" routerLinkActive="active">
<i class="nav-icon icon-schemas"></i> <div class="nav-text">Schemas</div>
</a>
</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">
<i class="nav-icon icon-contents"></i> <div class="nav-text">Content</div>
</a>
</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">
<i class="nav-icon icon-assets"></i> <div class="nav-text">Assets</div>
</a>
</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">
<i class="nav-icon icon-rules"></i> <div class="nav-text">Rules</div>
</a>
</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">
<i class="nav-icon icon-settings"></i> <div class="nav-text">Settings</div>
</a>
</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">
<i class="nav-icon icon-api"></i> <div class="nav-text">API</div>
</a>

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

@ -16,7 +16,8 @@ import { AppsState } from '@app/shared';
changeDetection: ChangeDetectionStrategy.OnPush
})
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>
</li>
<ng-container *ngIf="appsState.selectedApp | async; let selectedApp">
<ng-container *ngIf="selectedApp.planUpgrade && selectedApp.planName">
<li class="nav-item" *sqxPermission="'squidex.apps.{app}.plans.change'">
<div class="btn-group app-upgrade">
<button type="button" class="btn btn-primary btn-plan">
You are on the <strong>{{selectedApp.planName}}</strong> plan.
</button>
<button type="button" class="btn btn-warning" [routerLink]="['/app', selectedApp.name, 'settings', 'plans']">
Upgrade!
</button>
</div>
</li>
</ng-container>
<ng-container *ngIf="appsState.selectedValidApp | async; let selectedApp">
<li class="nav-item" *ngIf="selectedApp.planUpgrade && selectedApp.planName">
<div class="btn-group app-upgrade">
<button type="button" class="btn btn-primary btn-plan">
You are on the <strong>{{selectedApp.planName}}</strong> plan.
</button>
<button type="button" class="btn btn-warning" [routerLink]="['/app', selectedApp.name, 'settings', 'plans']">
Upgrade!
</button>
</div>
</li>
</ng-container>
</ul>

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

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

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

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

Loading…
Cancel
Save