Browse Source

Merge branch 'context' into workflow-ui

pull/380/head
Sebastian Stehle 7 years ago
parent
commit
e3d6f68e96
  1. 8
      src/Squidex.Domain.Apps.Entities/Assets/AssetQueryService.cs
  2. 2
      src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs
  3. 20
      src/Squidex.Domain.Apps.Entities/Contents/ContentEnricher.cs
  4. 70
      src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs
  5. 139
      src/Squidex.Domain.Apps.Entities/Contents/ContextExtensions.cs
  6. 4
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs
  7. 2
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs
  8. 4
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLService.cs
  9. 8
      src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs
  10. 4
      src/Squidex.Domain.Apps.Entities/Contents/QueryExecutionContext.cs
  11. 45
      src/Squidex.Domain.Apps.Entities/Context.cs
  12. 7
      src/Squidex.Domain.Apps.Entities/IContextProvider.cs
  13. 116
      src/Squidex.Domain.Apps.Entities/QueryContext.cs
  14. 12
      src/Squidex.Web/ApiController.cs
  15. 25
      src/Squidex.Web/ApiPermissionAttribute.cs
  16. 18
      src/Squidex.Web/CommandMiddlewares/EnrichWithAppIdCommandMiddleware.cs
  17. 7
      src/Squidex.Web/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs
  18. 37
      src/Squidex.Web/ContextExtensions.cs
  19. 30
      src/Squidex.Web/ContextProvider.cs
  20. 2
      src/Squidex.Web/EntityCreatedDto.cs
  21. 4
      src/Squidex.Web/ErrorDto.cs
  22. 16
      src/Squidex.Web/IAppFeature.cs
  23. 22
      src/Squidex.Web/PermissionExtensions.cs
  24. 8
      src/Squidex.Web/Pipeline/ApiCostsFilter.cs
  25. 18
      src/Squidex.Web/Pipeline/AppResolver.cs
  26. 10
      src/Squidex.Web/Pipeline/RequestLogPerformanceMiddleware.cs
  27. 4
      src/Squidex.Web/Resource.cs
  28. 2
      src/Squidex.Web/ResourceLink.cs
  29. 2
      src/Squidex.Web/Squidex.Web.csproj
  30. 6
      src/Squidex.Web/UrlHelperExtensions.cs
  31. 9
      src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs
  32. 47
      src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs
  33. 4
      src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs
  34. 2
      src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs
  35. 3
      src/Squidex/Areas/Api/Controllers/UI/UIController.cs
  36. 4
      src/Squidex/Config/Web/WebServices.cs
  37. 34
      src/Squidex/app/features/administration/services/event-consumers.service.ts
  38. 50
      src/Squidex/app/features/administration/services/users.service.ts
  39. 50
      src/Squidex/app/shared/services/app-languages.service.ts
  40. 32
      src/Squidex/app/shared/services/apps.service.ts
  41. 150
      src/Squidex/app/shared/services/assets.service.ts
  42. 20
      src/Squidex/app/shared/services/auth.service.ts
  43. 60
      src/Squidex/app/shared/services/backups.service.ts
  44. 58
      src/Squidex/app/shared/services/clients.service.ts
  45. 70
      src/Squidex/app/shared/services/comments.service.ts
  46. 132
      src/Squidex/app/shared/services/contents.service.ts
  47. 42
      src/Squidex/app/shared/services/contributors.service.ts
  48. 2
      src/Squidex/app/shared/services/help.service.ts
  49. 24
      src/Squidex/app/shared/services/history.service.ts
  50. 16
      src/Squidex/app/shared/services/languages.service.ts
  51. 28
      src/Squidex/app/shared/services/news.service.ts
  52. 56
      src/Squidex/app/shared/services/plans.service.ts
  53. 52
      src/Squidex/app/shared/services/roles.service.ts
  54. 3
      src/Squidex/app/shared/services/rules.service.ts
  55. 8
      src/Squidex/app/shared/services/translations.service.ts
  56. 62
      src/Squidex/app/shared/services/usages.service.ts
  57. 4
      tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetQueryServiceTests.cs
  58. 36
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentEnricherTests.cs
  59. 32
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs
  60. 8
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs
  61. 4
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs
  62. 2
      tests/Squidex.Web.Tests/ApiPermissionAttributeTests.cs
  63. 31
      tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs
  64. 40
      tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithSchemaIdCommandMiddlewareTests.cs
  65. 2
      tests/Squidex.Web.Tests/Pipeline/ApiCostsFilterTests.cs
  66. 6
      tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs

8
src/Squidex.Domain.Apps.Entities/Assets/AssetQueryService.cs

@ -70,7 +70,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
return await assetEnricher.EnrichAsync(assets);
}
public async Task<IResultList<IEnrichedAssetEntity>> QueryAsync(QueryContext context, Q query)
public async Task<IResultList<IEnrichedAssetEntity>> QueryAsync(Context context, Q query)
{
Guard.NotNull(context, nameof(context));
Guard.NotNull(query, nameof(query));
@ -91,14 +91,14 @@ namespace Squidex.Domain.Apps.Entities.Assets
return ResultList.Create(assets.Total, enriched);
}
private async Task<IResultList<IAssetEntity>> QueryByQueryAsync(QueryContext context, Q query)
private async Task<IResultList<IAssetEntity>> QueryByQueryAsync(Context context, Q query)
{
var parsedQuery = ParseQuery(context, query.ODataQuery);
return await assetRepository.QueryAsync(context.App.Id, parsedQuery);
}
private async Task<IResultList<IAssetEntity>> QueryByIdsAsync(QueryContext context, Q query)
private async Task<IResultList<IAssetEntity>> QueryByIdsAsync(Context context, Q query)
{
var assets = await assetRepository.QueryAsync(context.App.Id, new HashSet<Guid>(query.Ids));
@ -110,7 +110,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
return assets.SortSet(x => x.Id, ids);
}
private Query ParseQuery(QueryContext context, string query)
private Query ParseQuery(Context context, string query)
{
try
{

2
src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs

@ -18,7 +18,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
Task<IReadOnlyList<IEnrichedAssetEntity>> QueryByHashAsync(Guid appId, string hash);
Task<IResultList<IEnrichedAssetEntity>> QueryAsync(QueryContext contex, Q query);
Task<IResultList<IEnrichedAssetEntity>> QueryAsync(Context contex, Q query);
Task<IEnrichedAssetEntity> FindAssetAsync(Guid id);
}

20
src/Squidex.Domain.Apps.Entities/Contents/ContentEnricher.cs

@ -21,10 +21,15 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
private const string DefaultColor = StatusColors.Draft;
private readonly IContentWorkflow contentWorkflow;
private readonly IContextProvider contextProvider;
public ContentEnricher(IContentWorkflow contentWorkflow)
public ContentEnricher(IContentWorkflow contentWorkflow, IContextProvider contextProvider)
{
Guard.NotNull(contentWorkflow, nameof(contentWorkflow));
Guard.NotNull(contextProvider, nameof(contextProvider));
this.contentWorkflow = contentWorkflow;
this.contextProvider = contextProvider;
}
public async Task<IEnrichedContentEntity> EnrichAsync(IContentEntity content)
@ -51,8 +56,12 @@ namespace Squidex.Domain.Apps.Entities.Contents
var result = SimpleMapper.Map(content, new ContentEntity());
await ResolveColorAsync(content, result, cache);
await ResolveNextsAsync(content, result);
await ResolveCanUpdateAsync(content, result);
if (ShouldEnrichWithStatuses())
{
await ResolveNextsAsync(content, result);
await ResolveCanUpdateAsync(content, result);
}
results.Add(result);
}
@ -61,6 +70,11 @@ namespace Squidex.Domain.Apps.Entities.Contents
}
}
private bool ShouldEnrichWithStatuses()
{
return contextProvider.Context.IsFrontendClient || contextProvider.Context.IsResolveFlow();
}
private async Task ResolveCanUpdateAsync(IContentEntity content, ContentEntity result)
{
result.CanUpdate = await contentWorkflow.CanUpdateAsync(content);

70
src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs

@ -78,13 +78,13 @@ namespace Squidex.Domain.Apps.Entities.Contents
this.scriptEngine = scriptEngine;
}
public async Task<IEnrichedContentEntity> FindContentAsync(QueryContext context, string schemaIdOrName, Guid id, long version = -1)
public async Task<IEnrichedContentEntity> FindContentAsync(Context context, string schemaIdOrName, Guid id, long version = -1)
{
Guard.NotNull(context, nameof(context));
var schema = await GetSchemaOrThrowAsync(context, schemaIdOrName);
CheckPermission(context.User, schema);
CheckPermission(context, schema);
using (Profiler.TraceMethod<ContentQueryService>())
{
@ -108,13 +108,13 @@ namespace Squidex.Domain.Apps.Entities.Contents
}
}
public async Task<IResultList<IEnrichedContentEntity>> QueryAsync(QueryContext context, string schemaIdOrName, Q query)
public async Task<IResultList<IEnrichedContentEntity>> QueryAsync(Context context, string schemaIdOrName, Q query)
{
Guard.NotNull(context, nameof(context));
var schema = await GetSchemaOrThrowAsync(context, schemaIdOrName);
CheckPermission(context.User, schema);
CheckPermission(context, schema);
using (Profiler.TraceMethod<ContentQueryService>())
{
@ -133,7 +133,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
}
}
public async Task<IResultList<IEnrichedContentEntity>> QueryAsync(QueryContext context, IReadOnlyList<Guid> ids)
public async Task<IResultList<IEnrichedContentEntity>> QueryAsync(Context context, IReadOnlyList<Guid> ids)
{
Guard.NotNull(context, nameof(context));
@ -148,13 +148,11 @@ namespace Squidex.Domain.Apps.Entities.Contents
var contents = await QueryCoreAsync(context, ids);
var permissions = context.User.Permissions();
foreach (var group in contents.GroupBy(x => x.Schema.Id))
{
var schema = group.First().Schema;
if (HasPermission(permissions, schema))
if (HasPermission(context, schema))
{
var enriched = await TransformCoreAsync(context, schema, group.Select(x => x.Content));
@ -166,21 +164,21 @@ namespace Squidex.Domain.Apps.Entities.Contents
}
}
private async Task<IResultList<IEnrichedContentEntity>> TransformAsync(QueryContext context, ISchemaEntity schema, IResultList<IContentEntity> contents)
private async Task<IResultList<IEnrichedContentEntity>> TransformAsync(Context context, ISchemaEntity schema, IResultList<IContentEntity> contents)
{
var transformed = await TransformCoreAsync(context, schema, contents);
return ResultList.Create(contents.Total, transformed);
}
private async Task<IEnrichedContentEntity> TransformAsync(QueryContext context, ISchemaEntity schema, IContentEntity content)
private async Task<IEnrichedContentEntity> TransformAsync(Context context, ISchemaEntity schema, IContentEntity content)
{
var transformed = await TransformCoreAsync(context, schema, Enumerable.Repeat(content, 1));
return transformed[0];
}
private async Task<IReadOnlyList<IEnrichedContentEntity>> TransformCoreAsync(QueryContext context, ISchemaEntity schema, IEnumerable<IContentEntity> contents)
private async Task<IReadOnlyList<IEnrichedContentEntity>> TransformCoreAsync(Context context, ISchemaEntity schema, IEnumerable<IContentEntity> contents)
{
using (Profiler.TraceMethod<ContentQueryService>())
{
@ -209,7 +207,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
result.Data = result.Data.ConvertName2Name(schema.SchemaDef, converters);
}
if (result.DataDraft != null && (context.ApiStatus == StatusForApi.All || context.IsFrontendClient))
if (result.DataDraft != null && (context.IsUnpublished() || context.IsFrontendClient))
{
result.DataDraft = result.DataDraft.ConvertName2Name(schema.SchemaDef, converters);
}
@ -225,7 +223,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
}
}
private IEnumerable<FieldConverter> GenerateConverters(QueryContext context)
private IEnumerable<FieldConverter> GenerateConverters(Context context)
{
if (!context.IsFrontendClient)
{
@ -243,19 +241,23 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
yield return FieldConverters.ResolveFallbackLanguages(context.App.LanguagesConfig);
if (context.Languages?.Any() == true)
var languages = context.Languages();
if (languages.Any())
{
yield return FieldConverters.FilterLanguages(context.App.LanguagesConfig, context.Languages);
yield return FieldConverters.FilterLanguages(context.App.LanguagesConfig, languages);
}
if (context.AssetUrlsToResolve?.Any() == true)
var assetUrls = context.AssetUrls();
if (assetUrls.Any() == true)
{
yield return FieldConverters.ResolveAssetUrls(context.AssetUrlsToResolve, assetUrlGenerator);
yield return FieldConverters.ResolveAssetUrls(assetUrls.ToList(), assetUrlGenerator);
}
}
}
private Query ParseQuery(QueryContext context, string query, ISchemaEntity schema)
private Query ParseQuery(Context context, string query, ISchemaEntity schema)
{
using (Profiler.TraceMethod<ContentQueryService>())
{
@ -292,7 +294,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
}
}
public async Task<ISchemaEntity> GetSchemaOrThrowAsync(QueryContext context, string schemaIdOrName)
public async Task<ISchemaEntity> GetSchemaOrThrowAsync(Context context, string schemaIdOrName)
{
ISchemaEntity schema = null;
@ -314,29 +316,27 @@ namespace Squidex.Domain.Apps.Entities.Contents
return schema;
}
private static void CheckPermission(ClaimsPrincipal user, params ISchemaEntity[] schemas)
private static void CheckPermission(Context context, params ISchemaEntity[] schemas)
{
var permissions = user.Permissions();
foreach (var schema in schemas)
{
if (!HasPermission(permissions, schema))
if (!HasPermission(context, schema))
{
throw new DomainForbiddenException("You do not have permission for this schema.");
}
}
}
private static bool HasPermission(PermissionSet permissions, ISchemaEntity schema)
private static bool HasPermission(Context context, ISchemaEntity schema)
{
var permission = Permissions.ForApp(Permissions.AppContentsRead, schema.AppId.Name, schema.SchemaDef.Name);
return permissions.Allows(permission);
return context.Permissions.Allows(permission);
}
private static Status[] GetStatus(QueryContext context)
private static Status[] GetStatus(Context context)
{
if (context.IsFrontendClient || context.ApiStatus == StatusForApi.All)
if (context.IsFrontendClient || context.IsUnpublished())
{
return null;
}
@ -346,36 +346,36 @@ namespace Squidex.Domain.Apps.Entities.Contents
}
}
private async Task<IResultList<IContentEntity>> QueryByQueryAsync(QueryContext context, Q query, ISchemaEntity schema)
private async Task<IResultList<IContentEntity>> QueryByQueryAsync(Context context, Q query, ISchemaEntity schema)
{
var parsedQuery = ParseQuery(context, query.ODataQuery, schema);
return await QueryCoreAsync(context, schema, parsedQuery);
}
private async Task<IResultList<IContentEntity>> QueryByIdsAsync(QueryContext context, Q query, ISchemaEntity schema)
private async Task<IResultList<IContentEntity>> QueryByIdsAsync(Context context, Q query, ISchemaEntity schema)
{
var contents = await QueryCoreAsync(context, schema, query.Ids.ToHashSet());
return contents.SortSet(x => x.Id, query.Ids);
}
private Task<List<(IContentEntity Content, ISchemaEntity Schema)>> QueryCoreAsync(QueryContext context, IReadOnlyList<Guid> ids)
private Task<List<(IContentEntity Content, ISchemaEntity Schema)>> QueryCoreAsync(Context context, IReadOnlyList<Guid> ids)
{
return contentRepository.QueryAsync(context.App, GetStatus(context), new HashSet<Guid>(ids), WithDraft(context));
}
private Task<IResultList<IContentEntity>> QueryCoreAsync(QueryContext context, ISchemaEntity schema, Query query)
private Task<IResultList<IContentEntity>> QueryCoreAsync(Context context, ISchemaEntity schema, Query query)
{
return contentRepository.QueryAsync(context.App, schema, GetStatus(context), context.IsFrontendClient, query, WithDraft(context));
}
private Task<IResultList<IContentEntity>> QueryCoreAsync(QueryContext context, ISchemaEntity schema, HashSet<Guid> ids)
private Task<IResultList<IContentEntity>> QueryCoreAsync(Context context, ISchemaEntity schema, HashSet<Guid> ids)
{
return contentRepository.QueryAsync(context.App, schema, GetStatus(context), ids, WithDraft(context));
}
private Task<IContentEntity> FindCoreAsync(QueryContext context, Guid id, ISchemaEntity schema)
private Task<IContentEntity> FindCoreAsync(Context context, Guid id, ISchemaEntity schema)
{
return contentRepository.FindContentAsync(context.App, schema, GetStatus(context), id, WithDraft(context));
}
@ -385,9 +385,9 @@ namespace Squidex.Domain.Apps.Entities.Contents
return contentVersionLoader.LoadAsync(id, version);
}
private static bool WithDraft(QueryContext context)
private static bool WithDraft(Context context)
{
return context.ApiStatus == StatusForApi.All || context.IsFrontendClient;
return context.IsUnpublished() || context.IsFrontendClient;
}
}
}

139
src/Squidex.Domain.Apps.Entities/Contents/ContextExtensions.cs

@ -0,0 +1,139 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents
{
public static class ContextExtensions
{
private const string HeaderUnpublished = "X-Unpublished";
private const string HeaderFlatten = "X-Flatten";
private const string HeaderLanguages = "X-Languages";
private const string HeaderResolveFlow = "X-ResolveFlow";
private const string HeaderResolveAssetUrls = "X-Resolve-Urls";
private static readonly char[] Separators = { ',', ';' };
public static bool IsUnpublished(this Context context)
{
return context.Headers.ContainsKey(HeaderUnpublished);
}
public static Context WithUnpublished(this Context context, bool value = true)
{
if (value)
{
context.Headers[HeaderUnpublished] = "1";
}
else
{
context.Headers.Remove(HeaderUnpublished);
}
return context;
}
public static bool IsFlatten(this Context context)
{
return context.Headers.ContainsKey(HeaderFlatten);
}
public static Context WithFlatten(this Context context, bool value = true)
{
if (value)
{
context.Headers[HeaderFlatten] = "1";
}
else
{
context.Headers.Remove(HeaderFlatten);
}
return context;
}
public static bool IsResolveFlow(this Context context)
{
return context.Headers.ContainsKey(HeaderResolveFlow);
}
public static Context WithResolveFlow(this Context context, bool value = true)
{
if (value)
{
context.Headers[HeaderResolveFlow] = "1";
}
else
{
context.Headers.Remove(HeaderResolveFlow);
}
return context;
}
public static IEnumerable<string> AssetUrls(this Context context)
{
if (context.Headers.TryGetValue(HeaderResolveAssetUrls, out var value))
{
return value.Split(Separators, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).ToHashSet();
}
return Enumerable.Empty<string>();
}
public static Context WithAssetUrlsToResolve(this Context context, IEnumerable<string> fieldNames)
{
if (fieldNames?.Any() == true)
{
context.Headers[HeaderResolveAssetUrls] = string.Join(",", fieldNames);
}
else
{
context.Headers.Remove(HeaderResolveAssetUrls);
}
return context;
}
public static IEnumerable<Language> Languages(this Context context)
{
if (context.Headers.TryGetValue(HeaderResolveAssetUrls, out var value))
{
var languages = new HashSet<Language>();
foreach (var iso2Code in value.Split(Separators, StringSplitOptions.RemoveEmptyEntries))
{
if (Language.TryGetLanguage(iso2Code.Trim(), out var language))
{
languages.Add(language);
}
}
return languages;
}
return Enumerable.Empty<Language>();
}
public static Context WithLanguages(this Context context, IEnumerable<string> fieldNames)
{
if (fieldNames?.Any() == true)
{
context.Headers[HeaderLanguages] = string.Join(",", fieldNames);
}
else
{
context.Headers.Remove(HeaderLanguages);
}
return context;
}
}
}

4
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs

@ -29,7 +29,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
this.resolver = resolver;
}
public async Task<(bool HasError, object Response)> QueryAsync(QueryContext context, params GraphQLQuery[] queries)
public async Task<(bool HasError, object Response)> QueryAsync(Context context, params GraphQLQuery[] queries)
{
Guard.NotNull(context, nameof(context));
Guard.NotNull(queries, nameof(queries));
@ -43,7 +43,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
return (result.Any(x => x.HasError), result.ToArray(x => x.Response));
}
public async Task<(bool HasError, object Response)> QueryAsync(QueryContext context, GraphQLQuery query)
public async Task<(bool HasError, object Response)> QueryAsync(Context context, GraphQLQuery query)
{
Guard.NotNull(context, nameof(context));
Guard.NotNull(query, nameof(query));

2
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs

@ -29,7 +29,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
public ISemanticLog Log { get; }
public GraphQLExecutionContext(QueryContext context, IDependencyResolver resolver)
public GraphQLExecutionContext(Context context, IDependencyResolver resolver)
: base(context,
resolver.Resolve<IAssetQueryService>(),
resolver.Resolve<IContentQueryService>())

4
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLService.cs

@ -11,8 +11,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
public interface IGraphQLService
{
Task<(bool HasError, object Response)> QueryAsync(QueryContext context, params GraphQLQuery[] queries);
Task<(bool HasError, object Response)> QueryAsync(Context context, params GraphQLQuery[] queries);
Task<(bool HasError, object Response)> QueryAsync(QueryContext context, GraphQLQuery query);
Task<(bool HasError, object Response)> QueryAsync(Context context, GraphQLQuery query);
}
}

8
src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs

@ -17,12 +17,12 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
int DefaultPageSizeGraphQl { get; }
Task<IResultList<IEnrichedContentEntity>> QueryAsync(QueryContext context, IReadOnlyList<Guid> ids);
Task<IResultList<IEnrichedContentEntity>> QueryAsync(Context context, IReadOnlyList<Guid> ids);
Task<IResultList<IEnrichedContentEntity>> QueryAsync(QueryContext context, string schemaIdOrName, Q query);
Task<IResultList<IEnrichedContentEntity>> QueryAsync(Context context, string schemaIdOrName, Q query);
Task<IEnrichedContentEntity> FindContentAsync(QueryContext context, string schemaIdOrName, Guid id, long version = EtagVersion.Any);
Task<IEnrichedContentEntity> FindContentAsync(Context context, string schemaIdOrName, Guid id, long version = EtagVersion.Any);
Task<ISchemaEntity> GetSchemaOrThrowAsync(QueryContext context, string schemaIdOrName);
Task<ISchemaEntity> GetSchemaOrThrowAsync(Context context, string schemaIdOrName);
}
}

4
src/Squidex.Domain.Apps.Entities/Contents/QueryExecutionContext.cs

@ -21,9 +21,9 @@ namespace Squidex.Domain.Apps.Entities.Contents
private readonly ConcurrentDictionary<Guid, IEnrichedAssetEntity> cachedAssets = new ConcurrentDictionary<Guid, IEnrichedAssetEntity>();
private readonly IContentQueryService contentQuery;
private readonly IAssetQueryService assetQuery;
private readonly QueryContext context;
private readonly Context context;
public QueryExecutionContext(QueryContext context, IAssetQueryService assetQuery, IContentQueryService contentQuery)
public QueryExecutionContext(Context context, IAssetQueryService assetQuery, IContentQueryService contentQuery)
{
Guard.NotNull(assetQuery, nameof(assetQuery));
Guard.NotNull(contentQuery, nameof(contentQuery));

45
src/Squidex.Domain.Apps.Entities/Context.cs

@ -0,0 +1,45 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Security.Claims;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Infrastructure.Security;
using Squidex.Shared.Identity;
namespace Squidex.Domain.Apps.Entities
{
public sealed class Context
{
public IDictionary<string, string> Headers { get; } = new Dictionary<string, string>();
public IAppEntity App { get; set; }
public ClaimsPrincipal User { get; set; }
public PermissionSet Permissions
{
get { return User?.Permissions() ?? PermissionSet.Empty; }
}
public Context()
{
}
public Context(ClaimsPrincipal user, IAppEntity app)
{
User = user;
App = app;
}
public bool IsFrontendClient
{
get { return User != null && User.IsInClient("squidex-frontend"); }
}
}
}

7
src/Squidex.Domain.Apps.Entities/Contents/StatusForApi.cs → src/Squidex.Domain.Apps.Entities/IContextProvider.cs

@ -5,11 +5,10 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Domain.Apps.Entities.Contents
namespace Squidex.Domain.Apps.Entities
{
public enum StatusForApi
public interface IContextProvider
{
PublishedOnly,
All,
Context Context { get; }
}
}

116
src/Squidex.Domain.Apps.Entities/QueryContext.cs

@ -1,116 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Security.Claims;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Security;
namespace Squidex.Domain.Apps.Entities
{
public sealed class QueryContext : Cloneable<QueryContext>
{
private static readonly char[] Separators = { ',', ';' };
public ClaimsPrincipal User { get; private set; }
public IAppEntity App { get; private set; }
public bool Flatten { get; set; }
public StatusForApi ApiStatus { get; private set; }
public IReadOnlyCollection<string> AssetUrlsToResolve { get; private set; }
public IReadOnlyCollection<Language> Languages { get; private set; }
private QueryContext()
{
}
public static QueryContext Create(IAppEntity app, ClaimsPrincipal user)
{
return new QueryContext { App = app, User = user };
}
public QueryContext WithFlatten(bool flatten)
{
return Clone(c => c.Flatten = flatten);
}
public QueryContext WithUnpublished(bool unpublished)
{
return WithApiStatus(unpublished ? StatusForApi.All : StatusForApi.PublishedOnly);
}
public QueryContext WithApiStatus(StatusForApi status)
{
return Clone(c => c.ApiStatus = status);
}
public QueryContext WithAssetUrlsToResolve(IEnumerable<string> fieldNames)
{
if (fieldNames != null)
{
return Clone(c =>
{
var fields = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
c.AssetUrlsToResolve?.Foreach(x => fields.Add(x));
foreach (var part in fieldNames)
{
foreach (var fieldName in part.Split(Separators, StringSplitOptions.RemoveEmptyEntries))
{
fields.Add(fieldName.Trim());
}
}
c.AssetUrlsToResolve = fields;
});
}
return this;
}
public QueryContext WithLanguages(IEnumerable<string> languageCodes)
{
if (languageCodes != null)
{
return Clone(c =>
{
var languages = new HashSet<Language>();
c.Languages?.Foreach(x => languages.Add(x));
foreach (var part in languageCodes)
{
foreach (var iso2Code in part.Split(Separators, StringSplitOptions.RemoveEmptyEntries))
{
if (Language.TryGetLanguage(iso2Code.Trim(), out var language))
{
languages.Add(language);
}
}
}
c.Languages = languages;
});
}
return this;
}
public bool IsFrontendClient
{
get { return User.IsInClient("squidex-frontend"); }
}
}
}

12
src/Squidex.Web/ApiController.cs

@ -8,6 +8,7 @@
using System;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
@ -26,17 +27,22 @@ namespace Squidex.Web
{
get
{
var appFeature = HttpContext.Features.Get<IAppFeature>();
var app = HttpContext.Context().App;
if (appFeature == null)
if (app == null)
{
throw new InvalidOperationException("Not in a app context.");
}
return appFeature.App;
return app;
}
}
protected Context Context
{
get { return HttpContext.Context(); }
}
protected Guid AppId
{
get { return App.Id; }

25
src/Squidex.Web/ApiPermissionAttribute.cs

@ -36,23 +36,26 @@ namespace Squidex.Web
{
if (permissionIds.Length > 0)
{
var set = context.HttpContext.User.Permissions();
var permissions = context.HttpContext.Context().Permissions;
var hasPermission = false;
foreach (var permissionId in permissionIds)
if (permissions != null)
{
var id = permissionId;
foreach (var routeParam in context.RouteData.Values)
foreach (var permissionId in permissionIds)
{
id = id.Replace($"{{{routeParam.Key}}}", routeParam.Value?.ToString());
}
var id = permissionId;
if (set.Allows(new Permission(id)))
{
hasPermission = true;
break;
foreach (var routeParam in context.RouteData.Values)
{
id = id.Replace($"{{{routeParam.Key}}}", routeParam.Value?.ToString());
}
if (permissions.Allows(new Permission(id)))
{
hasPermission = true;
break;
}
}
}

18
src/Squidex.Web/CommandMiddlewares/EnrichWithAppIdCommandMiddleware.cs

@ -7,7 +7,6 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure;
@ -17,20 +16,15 @@ namespace Squidex.Web.CommandMiddlewares
{
public sealed class EnrichWithAppIdCommandMiddleware : ICommandMiddleware
{
private readonly IHttpContextAccessor httpContextAccessor;
private readonly IContextProvider contextProvider;
public EnrichWithAppIdCommandMiddleware(IHttpContextAccessor httpContextAccessor)
public EnrichWithAppIdCommandMiddleware(IContextProvider contextProvider)
{
this.httpContextAccessor = httpContextAccessor;
this.contextProvider = contextProvider;
}
public Task HandleAsync(CommandContext context, Func<Task> next)
{
if (httpContextAccessor.HttpContext == null)
{
return next();
}
if (context.Command is IAppCommand appCommand && appCommand.AppId == null)
{
var appId = GetAppId();
@ -50,14 +44,14 @@ namespace Squidex.Web.CommandMiddlewares
private NamedId<Guid> GetAppId()
{
var appFeature = httpContextAccessor.HttpContext.Features.Get<IAppFeature>();
var context = contextProvider.Context;
if (appFeature?.App == null)
if (context.App == null)
{
throw new InvalidOperationException("Cannot resolve app.");
}
return appFeature.App.NamedId();
return context.App.NamedId();
}
}
}

7
src/Squidex.Web/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs

@ -65,12 +65,7 @@ namespace Squidex.Web.CommandMiddlewares
if (appId == null)
{
var appFeature = actionContextAccessor.ActionContext.HttpContext.Features.Get<IAppFeature>();
if (appFeature?.App != null)
{
appId = appFeature.App.NamedId();
}
appId = actionContextAccessor.ActionContext.HttpContext.Context().App?.NamedId();
}
if (appId != null)

37
src/Squidex.Web/ContextExtensions.cs

@ -0,0 +1,37 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.AspNetCore.Http;
using Squidex.Domain.Apps.Entities;
namespace Squidex.Web
{
public static class ContextExtensions
{
public static Context Context(this HttpContext httpContext)
{
var context = httpContext.Features.Get<Context>();
if (context == null)
{
context = new Context { User = httpContext.User };
foreach (var header in httpContext.Request.Headers)
{
if (header.Key.StartsWith("X-", System.StringComparison.Ordinal))
{
context.Headers.Add(header.Key, header.Value.ToString());
}
}
httpContext.Features.Set(context);
}
return context;
}
}
}

30
src/Squidex.Web/ContextProvider.cs

@ -0,0 +1,30 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.AspNetCore.Http;
using Squidex.Domain.Apps.Entities;
using Squidex.Infrastructure;
namespace Squidex.Web
{
public sealed class ContextProvider : IContextProvider
{
private readonly IHttpContextAccessor httpContextAccessor;
public Context Context
{
get { return httpContextAccessor.HttpContext.Context(); }
}
public ContextProvider(IHttpContextAccessor httpContextAccessor)
{
Guard.NotNull(httpContextAccessor, nameof(httpContextAccessor));
this.httpContextAccessor = httpContextAccessor;
}
}
}

2
src/Squidex.Web/EntityCreatedDto.cs

@ -15,7 +15,7 @@ namespace Squidex.Web
[Required]
[Display(Description = "Id of the created entity.")]
public string Id { get; set; }
[Display(Description = "The new version of the entity.")]
public long Version { get; set; }

4
src/Squidex.Web/ErrorDto.cs

@ -14,10 +14,10 @@ namespace Squidex.Web
[Required]
[Display(Description = "Error message.")]
public string Message { get; set; }
[Display(Description = "Detailed error messages.")]
public string[] Details { get; set; }
[Display(Description = "Status code of the http response.")]
public int? StatusCode { get; set; } = 400;
}

16
src/Squidex.Web/IAppFeature.cs

@ -1,16 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Entities.Apps;
namespace Squidex.Web
{
public interface IAppFeature
{
IAppEntity App { get; }
}
}

22
src/Squidex.Web/PermissionExtensions.cs

@ -7,34 +7,14 @@
using Microsoft.AspNetCore.Http;
using Squidex.Infrastructure.Security;
using Squidex.Shared.Identity;
namespace Squidex.Web
{
public static class PermissionExtensions
{
private sealed class PermissionFeature
{
public PermissionSet Permissions { get; }
public PermissionFeature(PermissionSet permissions)
{
Permissions = permissions;
}
}
public static PermissionSet Permissions(this HttpContext httpContext)
{
var feature = httpContext.Features.Get<PermissionFeature>();
if (feature == null)
{
feature = new PermissionFeature(httpContext.User.Permissions());
httpContext.Features.Set(feature);
}
return feature.Permissions;
return httpContext.Context().Permissions;
}
public static bool HasPermission(this HttpContext httpContext, Permission permission, PermissionSet permissions = null)

8
src/Squidex.Web/Pipeline/ApiCostsFilter.cs

@ -47,15 +47,15 @@ namespace Squidex.Web.Pipeline
{
context.HttpContext.Features.Set<IApiCostsFeature>(FilterDefinition);
var appFeature = context.HttpContext.Features.Get<IAppFeature>();
var app = context.HttpContext.Context().App;
if (appFeature?.App != null && FilterDefinition.Weight > 0)
if (app != null && FilterDefinition.Weight > 0)
{
var appId = appFeature.App.Id.ToString();
var appId = app.Id.ToString();
using (Profiler.Trace("CheckUsage"))
{
var plan = appPlansProvider.GetPlanForApp(appFeature.App);
var plan = appPlansProvider.GetPlanForApp(app);
var usage = await usageTracker.GetMonthlyCallsAsync(appId, DateTime.Today);

18
src/Squidex.Web/Pipeline/AppResolver.cs

@ -23,16 +23,6 @@ namespace Squidex.Web.Pipeline
{
private readonly IAppProvider appProvider;
public class AppFeature : IAppFeature
{
public IAppEntity App { get; }
public AppFeature(IAppEntity app)
{
App = app;
}
}
public AppResolver(IAppProvider appProvider)
{
this.appProvider = appProvider;
@ -76,15 +66,15 @@ namespace Squidex.Web.Pipeline
}
}
var set = user.Permissions();
var permissionSet = user.Permissions();
if (!set.Includes(Permissions.ForApp(Permissions.App, appName))&& !AllowAnonymous(context))
context.HttpContext.Context().App = app;
if (!permissionSet.Includes(Permissions.ForApp(Permissions.App, appName)) && !AllowAnonymous(context))
{
context.Result = new NotFoundResult();
return;
}
context.HttpContext.Features.Set<IAppFeature>(new AppFeature(app));
}
await next();

10
src/Squidex.Web/Pipeline/RequestLogPerformanceMiddleware.cs

@ -47,9 +47,9 @@ namespace Squidex.Web.Pipeline
}
}
private static void LogFilters(HttpContext context, IObjectWriter c)
private static void LogFilters(HttpContext httpContext, IObjectWriter c)
{
var app = context.Features.Get<IAppFeature>()?.App;
var app = httpContext.Context().App;
if (app != null)
{
@ -57,21 +57,21 @@ namespace Squidex.Web.Pipeline
c.WriteProperty("appName", app.Name);
}
var userId = context.User.OpenIdSubject();
var userId = httpContext.User.OpenIdSubject();
if (!string.IsNullOrWhiteSpace(userId))
{
c.WriteProperty(nameof(userId), userId);
}
var clientId = context.User.OpenIdClientId();
var clientId = httpContext.User.OpenIdClientId();
if (!string.IsNullOrWhiteSpace(clientId))
{
c.WriteProperty(nameof(clientId), clientId);
}
var costs = context.Features.Get<IApiCostsFeature>()?.Weight ?? 0;
var costs = httpContext.Features.Get<IApiCostsFeature>()?.Weight ?? 0;
c.WriteProperty(nameof(costs), costs);
}

4
src/Squidex.Web/Resource.cs

@ -5,10 +5,10 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Newtonsoft.Json;
using Squidex.Infrastructure;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Newtonsoft.Json;
using Squidex.Infrastructure;
namespace Squidex.Web
{

2
src/Squidex.Web/ResourceLink.cs

@ -17,7 +17,7 @@ namespace Squidex.Web
[Required]
[Display(Description = "The link method.")]
public string Method { get; set; }
public string Method { get; set; }
[Required]
[Display(Description = "Additional data about the link.")]

2
src/Squidex.Web/Squidex.Web.csproj

@ -9,6 +9,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Squidex.Domain.Apps.Entities\Squidex.Domain.Apps.Entities.csproj" />

6
src/Squidex.Web/UrlHelperExtensions.cs

@ -5,8 +5,10 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.AspNetCore.Mvc;
using System;
using Microsoft.AspNetCore.Mvc;
#pragma warning disable RECS0108 // Warns about static fields in generic types
namespace Squidex.Web
{
@ -22,7 +24,7 @@ namespace Squidex.Web
var name = typeof(T).Name;
if (name.EndsWith(suffix))
if (name.EndsWith(suffix, StringComparison.Ordinal))
{
name = name.Substring(0, name.Length - suffix.Length);
}

9
src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs

@ -101,9 +101,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
[ApiCosts(1)]
public async Task<IActionResult> GetAssets(string app, [FromQuery] string ids = null)
{
var context = Context();
var assets = await assetQuery.QueryAsync(context, Q.Empty.WithODataQuery(Request.QueryString.ToString()).WithIds(ids));
var assets = await assetQuery.QueryAsync(Context, Q.Empty.WithODataQuery(Request.QueryString.ToString()).WithIds(ids));
var response = AssetsDto.FromAssets(assets, this, app);
@ -304,10 +302,5 @@ namespace Squidex.Areas.Api.Controllers.Assets
return assetFile;
}
private QueryContext Context()
{
return QueryContext.Create(App, User);
}
}
}

47
src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs

@ -62,7 +62,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(2)]
public async Task<IActionResult> PostGraphQL(string app, [FromBody] GraphQLQuery query)
{
var result = await graphQl.QueryAsync(Context(), query);
var result = await graphQl.QueryAsync(Context, query);
if (result.HasError)
{
@ -93,7 +93,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(2)]
public async Task<IActionResult> PostGraphQLBatch(string app, [FromBody] GraphQLQuery[] batch)
{
var result = await graphQl.QueryAsync(Context(), batch);
var result = await graphQl.QueryAsync(Context, batch);
if (result.HasError)
{
@ -124,10 +124,9 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)]
public async Task<IActionResult> GetAllContents(string app, [FromQuery] string ids)
{
var context = Context();
var contents = await contentQuery.QueryAsync(context, Q.Empty.WithIds(ids).Ids);
var contents = await contentQuery.QueryAsync(Context, Q.Empty.WithIds(ids).Ids);
var response = await ContentsDto.FromContentsAsync(contents, context, this, null, contentWorkflow);
var response = await ContentsDto.FromContentsAsync(contents, Context, this, null, contentWorkflow);
if (controllerOptions.Value.EnableSurrogateKeys && response.Items.Length <= controllerOptions.Value.MaxItemsForSurrogateKeys)
{
@ -159,12 +158,11 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)]
public async Task<IActionResult> GetContents(string app, string name, [FromQuery] string ids = null)
{
var context = Context();
var contents = await contentQuery.QueryAsync(context, name, Q.Empty.WithIds(ids).WithODataQuery(Request.QueryString.ToString()));
var contents = await contentQuery.QueryAsync(Context, name, Q.Empty.WithIds(ids).WithODataQuery(Request.QueryString.ToString()));
var schema = await contentQuery.GetSchemaOrThrowAsync(context, name);
var schema = await contentQuery.GetSchemaOrThrowAsync(Context, name);
var response = await ContentsDto.FromContentsAsync(contents, context, this, schema, contentWorkflow);
var response = await ContentsDto.FromContentsAsync(contents, Context, this, schema, contentWorkflow);
if (ShouldProvideSurrogateKeys(response))
{
@ -196,10 +194,9 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)]
public async Task<IActionResult> GetContent(string app, string name, Guid id)
{
var context = Context();
var content = await contentQuery.FindContentAsync(context, name, id);
var content = await contentQuery.FindContentAsync(Context, name, id);
var response = ContentDto.FromContent(context, content, this);
var response = ContentDto.FromContent(Context, content, this);
if (controllerOptions.Value.EnableSurrogateKeys)
{
@ -232,10 +229,9 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)]
public async Task<IActionResult> GetContentVersion(string app, string name, Guid id, int version)
{
var context = Context();
var content = await contentQuery.FindContentAsync(context, name, id, version);
var content = await contentQuery.FindContentAsync(Context, name, id, version);
var response = ContentDto.FromContent(context, content, this);
var response = ContentDto.FromContent(Context, content, this);
if (controllerOptions.Value.EnableSurrogateKeys)
{
@ -269,7 +265,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)]
public async Task<IActionResult> PostContent(string app, string name, [FromBody] NamedContentData request, [FromQuery] bool publish = false)
{
await contentQuery.GetSchemaOrThrowAsync(Context(), name);
await contentQuery.GetSchemaOrThrowAsync(Context, name);
if (publish && !this.HasPermission(Helper.StatusPermission(app, name, Status.Published)))
{
@ -306,7 +302,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)]
public async Task<IActionResult> PutContent(string app, string name, Guid id, [FromBody] NamedContentData request, [FromQuery] bool asDraft = false)
{
await contentQuery.GetSchemaOrThrowAsync(Context(), name);
await contentQuery.GetSchemaOrThrowAsync(Context, name);
var command = new UpdateContent { ContentId = id, Data = request.ToCleaned(), AsDraft = asDraft };
@ -338,7 +334,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)]
public async Task<IActionResult> PatchContent(string app, string name, Guid id, [FromBody] NamedContentData request, [FromQuery] bool asDraft = false)
{
await contentQuery.GetSchemaOrThrowAsync(Context(), name);
await contentQuery.GetSchemaOrThrowAsync(Context, name);
var command = new PatchContent { ContentId = id, Data = request.ToCleaned(), AsDraft = asDraft };
@ -369,7 +365,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)]
public async Task<IActionResult> PutContentStatus(string app, string name, Guid id, ChangeStatusDto request)
{
await contentQuery.GetSchemaOrThrowAsync(Context(), name);
await contentQuery.GetSchemaOrThrowAsync(Context, name);
if (!this.HasPermission(Helper.StatusPermission(app, name, Status.Published)))
{
@ -404,7 +400,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)]
public async Task<IActionResult> DiscardDraft(string app, string name, Guid id)
{
await contentQuery.GetSchemaOrThrowAsync(Context(), name);
await contentQuery.GetSchemaOrThrowAsync(Context, name);
var command = new DiscardChanges { ContentId = id };
@ -432,7 +428,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)]
public async Task<IActionResult> DeleteContent(string app, string name, Guid id)
{
await contentQuery.GetSchemaOrThrowAsync(Context(), name);
await contentQuery.GetSchemaOrThrowAsync(Context, name);
var command = new DeleteContent { ContentId = id };
@ -451,15 +447,6 @@ namespace Squidex.Areas.Api.Controllers.Contents
return response;
}
private QueryContext Context()
{
return QueryContext.Create(App, User)
.WithAssetUrlsToResolve(Request.Headers["X-Resolve-Urls"])
.WithFlatten(Request.Headers.ContainsKey("X-Flatten"))
.WithLanguages(Request.Headers["X-Languages"])
.WithUnpublished(Request.Headers.ContainsKey("X-Unpublished"));
}
private bool ShouldProvideSurrogateKeys(ContentsDto response)
{
return controllerOptions.Value.EnableSurrogateKeys && response.Items.Length <= controllerOptions.Value.MaxItemsForSurrogateKeys;

4
src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs

@ -84,11 +84,11 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
/// </summary>
public long Version { get; set; }
public static ContentDto FromContent(QueryContext context, IEnrichedContentEntity content, ApiController controller)
public static ContentDto FromContent(Context context, IEnrichedContentEntity content, ApiController controller)
{
var response = SimpleMapper.Map(content, new ContentDto());
if (context?.Flatten == true)
if (context.IsFlatten())
{
response.Data = content.Data?.ToFlatten();
response.DataDraft = content.DataDraft?.ToFlatten();

2
src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs

@ -48,7 +48,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
}
public static async Task<ContentsDto> FromContentsAsync(IResultList<IEnrichedContentEntity> contents,
QueryContext context, ApiController controller, ISchemaEntity schema, IContentWorkflow contentWorkflow)
Context context, ApiController controller, ISchemaEntity schema, IContentWorkflow contentWorkflow)
{
var result = new ContentsDto
{

3
src/Squidex/Areas/Api/Controllers/UI/UIController.cs

@ -16,7 +16,6 @@ using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.Security;
using Squidex.Shared;
using Squidex.Shared.Identity;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.UI
@ -53,7 +52,7 @@ namespace Squidex.Areas.Api.Controllers.UI
MapKey = uiOptions.Map?.GoogleMaps?.Key
};
var canCreateApps = !uiOptions.OnlyAdminsCanCreateApps || User.Permissions().Includes(CreateAppPermission);
var canCreateApps = !uiOptions.OnlyAdminsCanCreateApps || Context.Permissions.Includes(CreateAppPermission);
result.CanCreateApps = canCreateApps;

4
src/Squidex/Config/Web/WebServices.cs

@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Config.Domain;
using Squidex.Domain.Apps.Entities;
using Squidex.Pipeline.Plugins;
using Squidex.Pipeline.Robots;
using Squidex.Web;
@ -41,6 +42,9 @@ namespace Squidex.Config.Web
services.AddSingletonAs<RequestLogPerformanceMiddleware>()
.AsSelf();
services.AddSingletonAs<ContextProvider>()
.As<IContextProvider>();
services.AddSingletonAs<ApiPermissionUnifier>()
.As<IClaimsTransformation>();

34
src/Squidex/app/features/administration/services/event-consumers.service.ts

@ -62,12 +62,12 @@ export class EventConsumersService {
const url = this.apiUrl.buildUrl('/api/event-consumers');
return this.http.get<{ items: any[] } & Resource>(url).pipe(
map(({ items, _links }) => {
const eventConsumers = items.map(item => parseEventConsumer(item));
map(({ items, _links }) => {
const eventConsumers = items.map(item => parseEventConsumer(item));
return new EventConsumersDto(eventConsumers, _links);
}),
pretifyError('Failed to load event consumers. Please reload.'));
return new EventConsumersDto(eventConsumers, _links);
}),
pretifyError('Failed to load event consumers. Please reload.'));
}
public putStart(eventConsumer: Resource): Observable<EventConsumerDto> {
@ -76,10 +76,10 @@ export class EventConsumersService {
const url = this.apiUrl.buildUrl(link.href);
return this.http.request(link.method, url).pipe(
map(body => {
return parseEventConsumer(body);
}),
pretifyError('Failed to start event consumer. Please reload.'));
map(body => {
return parseEventConsumer(body);
}),
pretifyError('Failed to start event consumer. Please reload.'));
}
public putStop(eventConsumer: Resource): Observable<EventConsumerDto> {
@ -88,10 +88,10 @@ export class EventConsumersService {
const url = this.apiUrl.buildUrl(link.href);
return this.http.request(link.method, url).pipe(
map(body => {
return parseEventConsumer(body);
}),
pretifyError('Failed to stop event consumer. Please reload.'));
map(body => {
return parseEventConsumer(body);
}),
pretifyError('Failed to stop event consumer. Please reload.'));
}
public putReset(eventConsumer: Resource): Observable<EventConsumerDto> {
@ -100,10 +100,10 @@ export class EventConsumersService {
const url = this.apiUrl.buildUrl(link.href);
return this.http.request(link.method, url).pipe(
map(body => {
return parseEventConsumer(body);
}),
pretifyError('Failed to reset event consumer. Please reload.'));
map(body => {
return parseEventConsumer(body);
}),
pretifyError('Failed to reset event consumer. Please reload.'));
}
}

50
src/Squidex/app/features/administration/services/users.service.ts

@ -73,32 +73,32 @@ export class UsersService {
const url = this.apiUrl.buildUrl(`api/user-management?take=${take}&skip=${skip}&query=${query || ''}`);
return this.http.get<{ total: number, items: any[] } & Resource>(url).pipe(
map(({ total, items, _links }) => {
const users = items.map(item => parseUser(item));
map(({ total, items, _links }) => {
const users = items.map(item => parseUser(item));
return new UsersDto(total, users, _links);
}),
pretifyError('Failed to load users. Please reload.'));
return new UsersDto(total, users, _links);
}),
pretifyError('Failed to load users. Please reload.'));
}
public getUser(id: string): Observable<UserDto> {
const url = this.apiUrl.buildUrl(`api/user-management/${id}`);
return this.http.get(url).pipe(
map(body => {
return parseUser(body);
}),
pretifyError('Failed to load user. Please reload.'));
map(body => {
return parseUser(body);
}),
pretifyError('Failed to load user. Please reload.'));
}
public postUser(dto: CreateUserDto): Observable<UserDto> {
const url = this.apiUrl.buildUrl('api/user-management');
return this.http.post(url, dto).pipe(
map(body => {
return parseUser(body);
}),
pretifyError('Failed to create user. Please reload.'));
map(body => {
return parseUser(body);
}),
pretifyError('Failed to create user. Please reload.'));
}
public putUser(user: Resource, dto: UpdateUserDto): Observable<UserDto> {
@ -107,10 +107,10 @@ export class UsersService {
const url = this.apiUrl.buildUrl(link.href);
return this.http.request(link.method, url, { body: dto }).pipe(
map(body => {
return parseUser(body);
}),
pretifyError('Failed to update user. Please reload.'));
map(body => {
return parseUser(body);
}),
pretifyError('Failed to update user. Please reload.'));
}
public lockUser(user: Resource): Observable<UserDto> {
@ -119,10 +119,10 @@ export class UsersService {
const url = this.apiUrl.buildUrl(link.href);
return this.http.request(link.method, url).pipe(
map(body => {
return parseUser(body);
}),
pretifyError('Failed to load users. Please retry.'));
map(body => {
return parseUser(body);
}),
pretifyError('Failed to load users. Please retry.'));
}
public unlockUser(user: Resource): Observable<UserDto> {
@ -131,10 +131,10 @@ export class UsersService {
const url = this.apiUrl.buildUrl(link.href);
return this.http.request(link.method, url).pipe(
map(body => {
return parseUser(body);
}),
pretifyError('Failed to load users. Please retry.'));
map(body => {
return parseUser(body);
}),
pretifyError('Failed to load users. Please retry.'));
}
}

50
src/Squidex/app/shared/services/app-languages.service.ts

@ -74,23 +74,23 @@ export class AppLanguagesService {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/languages`);
return HTTP.getVersioned(this.http, url).pipe(
mapVersioned(({ body }) => {
return parseLanguages(body);
}),
pretifyError('Failed to load languages. Please reload.'));
mapVersioned(({ body }) => {
return parseLanguages(body);
}),
pretifyError('Failed to load languages. Please reload.'));
}
public postLanguage(appName: string, dto: AddAppLanguageDto, version: Version): Observable<AppLanguagesDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/languages`);
return HTTP.postVersioned(this.http, url, dto, version).pipe(
mapVersioned(({ body }) => {
return parseLanguages(body);
}),
tap(() => {
this.analytics.trackEvent('Language', 'Added', appName);
}),
pretifyError('Failed to add language. Please reload.'));
mapVersioned(({ body }) => {
return parseLanguages(body);
}),
tap(() => {
this.analytics.trackEvent('Language', 'Added', appName);
}),
pretifyError('Failed to add language. Please reload.'));
}
public putLanguage(appName: string, resource: Resource, dto: UpdateAppLanguageDto, version: Version): Observable<AppLanguagesDto> {
@ -99,13 +99,13 @@ export class AppLanguagesService {
const url = this.apiUrl.buildUrl(link.href);
return HTTP.requestVersioned(this.http, link.method, url, version, dto).pipe(
mapVersioned(({ body }) => {
return parseLanguages(body);
}),
tap(() => {
this.analytics.trackEvent('Language', 'Updated', appName);
}),
pretifyError('Failed to change language. Please reload.'));
mapVersioned(({ body }) => {
return parseLanguages(body);
}),
tap(() => {
this.analytics.trackEvent('Language', 'Updated', appName);
}),
pretifyError('Failed to change language. Please reload.'));
}
public deleteLanguage(appName: string, resource: Resource, version: Version): Observable<AppLanguagesDto> {
@ -114,13 +114,13 @@ export class AppLanguagesService {
const url = this.apiUrl.buildUrl(link.href);
return HTTP.requestVersioned(this.http, link.method, url, version).pipe(
mapVersioned(({ body }) => {
return parseLanguages(body);
}),
tap(() => {
this.analytics.trackEvent('Language', 'Deleted', appName);
}),
pretifyError('Failed to add language. Please reload.'));
mapVersioned(({ body }) => {
return parseLanguages(body);
}),
tap(() => {
this.analytics.trackEvent('Language', 'Deleted', appName);
}),
pretifyError('Failed to add language. Please reload.'));
}
}

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

@ -86,25 +86,25 @@ export class AppsService {
const url = this.apiUrl.buildUrl('/api/apps');
return this.http.get<any[]>(url).pipe(
map(body => {
const apps = body.map(item => parseApp(item));
map(body => {
const apps = body.map(item => parseApp(item));
return apps;
}),
pretifyError('Failed to load apps. Please reload.'));
return apps;
}),
pretifyError('Failed to load apps. Please reload.'));
}
public postApp(dto: CreateAppDto): Observable<AppDto> {
const url = this.apiUrl.buildUrl('api/apps');
return this.http.post(url, dto).pipe(
map(body => {
return parseApp(body);
}),
tap(() => {
this.analytics.trackEvent('App', 'Created', dto.name);
}),
pretifyError('Failed to create app. Please reload.'));
map(body => {
return parseApp(body);
}),
tap(() => {
this.analytics.trackEvent('App', 'Created', dto.name);
}),
pretifyError('Failed to create app. Please reload.'));
}
public deleteApp(resource: Resource): Observable<any> {
@ -113,10 +113,10 @@ export class AppsService {
const url = this.apiUrl.buildUrl(link.href);
return this.http.request(link.method, url).pipe(
tap(() => {
this.analytics.trackEvent('App', 'Archived');
}),
pretifyError('Failed to archive app. Please reload.'));
tap(() => {
this.analytics.trackEvent('App', 'Archived');
}),
pretifyError('Failed to archive app. Please reload.'));
}
}

150
src/Squidex/app/shared/services/assets.service.ts

@ -138,12 +138,12 @@ export class AssetsService {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/assets?${fullQuery}`);
return this.http.get<{ total: number, items: any[] } & Resource>(url).pipe(
map(({ total, items, _links }) => {
const assets = items.map(item => parseAsset(item));
map(({ total, items, _links }) => {
const assets = items.map(item => parseAsset(item));
return new AssetsDto(total, assets, _links);
}),
pretifyError('Failed to load assets. Please reload.'));
return new AssetsDto(total, assets, _links);
}),
pretifyError('Failed to load assets. Please reload.'));
}
public uploadFile(appName: string, file: File): Observable<number | AssetDto> {
@ -152,45 +152,45 @@ export class AssetsService {
const req = new HttpRequest('POST', url, getFormData(file), { reportProgress: true });
return this.http.request(req).pipe(
filter(event =>
event.type === HttpEventType.UploadProgress ||
event.type === HttpEventType.Response),
map(event => {
if (event.type === HttpEventType.UploadProgress) {
const percentDone = event.total ? Math.round(100 * event.loaded / event.total) : 0;
return percentDone;
} else if (Types.is(event, HttpResponse)) {
return parseAsset(event.body);
} else {
throw 'Invalid';
}
}),
catchError((error: any) => {
if (Types.is(error, HttpErrorResponse) && error.status === 413) {
return throwError(new ErrorDto(413, 'Asset is too big.'));
} else {
return throwError(error);
}
}),
tap(value => {
if (!Types.isNumber(value)) {
this.analytics.trackEvent('Asset', 'Uploaded', appName);
}
}),
pretifyError('Failed to upload asset. Please reload.'));
filter(event =>
event.type === HttpEventType.UploadProgress ||
event.type === HttpEventType.Response),
map(event => {
if (event.type === HttpEventType.UploadProgress) {
const percentDone = event.total ? Math.round(100 * event.loaded / event.total) : 0;
return percentDone;
} else if (Types.is(event, HttpResponse)) {
return parseAsset(event.body);
} else {
throw 'Invalid';
}
}),
catchError((error: any) => {
if (Types.is(error, HttpErrorResponse) && error.status === 413) {
return throwError(new ErrorDto(413, 'Asset is too big.'));
} else {
return throwError(error);
}
}),
tap(value => {
if (!Types.isNumber(value)) {
this.analytics.trackEvent('Asset', 'Uploaded', appName);
}
}),
pretifyError('Failed to upload asset. Please reload.'));
}
public getAsset(appName: string, id: string): Observable<AssetDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/assets/${id}`);
return HTTP.getVersioned(this.http, url).pipe(
map(({ payload }) => {
const body = payload.body;
map(({ payload }) => {
const body = payload.body;
return parseAsset(body);
}),
pretifyError('Failed to load assets. Please reload.'));
return parseAsset(body);
}),
pretifyError('Failed to load assets. Please reload.'));
}
public replaceFile(appName: string, asset: Resource, file: File, version: Version): Observable<number | AssetDto> {
@ -201,33 +201,33 @@ export class AssetsService {
const req = new HttpRequest(link.method, url, getFormData(file), { headers: new HttpHeaders().set('If-Match', version.value), reportProgress: true });
return this.http.request(req).pipe(
filter(event =>
event.type === HttpEventType.UploadProgress ||
event.type === HttpEventType.Response),
map(event => {
if (event.type === HttpEventType.UploadProgress) {
const percentDone = event.total ? Math.round(100 * event.loaded / event.total) : 0;
return percentDone;
} else if (Types.is(event, HttpResponse)) {
return parseAsset(event.body);
} else {
throw 'Invalid';
}
}),
catchError(error => {
if (Types.is(error, HttpErrorResponse) && error.status === 413) {
return throwError(new ErrorDto(413, 'Asset is too big.'));
} else {
return throwError(error);
}
}),
tap(value => {
if (!Types.isNumber(value)) {
this.analytics.trackEvent('Analytics', 'Replaced', appName);
}
}),
pretifyError('Failed to replace asset. Please reload.'));
filter(event =>
event.type === HttpEventType.UploadProgress ||
event.type === HttpEventType.Response),
map(event => {
if (event.type === HttpEventType.UploadProgress) {
const percentDone = event.total ? Math.round(100 * event.loaded / event.total) : 0;
return percentDone;
} else if (Types.is(event, HttpResponse)) {
return parseAsset(event.body);
} else {
throw 'Invalid';
}
}),
catchError(error => {
if (Types.is(error, HttpErrorResponse) && error.status === 413) {
return throwError(new ErrorDto(413, 'Asset is too big.'));
} else {
return throwError(error);
}
}),
tap(value => {
if (!Types.isNumber(value)) {
this.analytics.trackEvent('Analytics', 'Replaced', appName);
}
}),
pretifyError('Failed to replace asset. Please reload.'));
}
public putAsset(appName: string, asset: Resource, dto: AnnotateAssetDto, version: Version): Observable<AssetDto> {
@ -236,13 +236,13 @@ export class AssetsService {
const url = this.apiUrl.buildUrl(link.href);
return HTTP.requestVersioned(this.http, link.method, url, version, dto).pipe(
map(({ payload }) => {
return parseAsset(payload.body);
}),
tap(() => {
this.analytics.trackEvent('Analytics', 'Updated', appName);
}),
pretifyError('Failed to update asset. Please reload.'));
map(({ payload }) => {
return parseAsset(payload.body);
}),
tap(() => {
this.analytics.trackEvent('Analytics', 'Updated', appName);
}),
pretifyError('Failed to update asset. Please reload.'));
}
public deleteAsset(appName: string, asset: Resource, version: Version): Observable<Versioned<any>> {
@ -251,10 +251,10 @@ export class AssetsService {
const url = this.apiUrl.buildUrl(link.href);
return HTTP.requestVersioned(this.http, link.method, url, version).pipe(
tap(() => {
this.analytics.trackEvent('Analytics', 'Deleted', appName);
}),
pretifyError('Failed to delete asset. Please reload.'));
tap(() => {
this.analytics.trackEvent('Analytics', 'Deleted', appName);
}),
pretifyError('Failed to delete asset. Please reload.'));
}
}

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

@ -75,15 +75,15 @@ export class AuthService {
Log.logger = console;
this.userManager = new UserManager({
client_id: 'squidex-frontend',
scope: 'squidex-api openid profile email squidex-profile role permissions',
response_type: 'id_token token',
redirect_uri: apiUrl.buildUrl('login;'),
post_logout_redirect_uri: apiUrl.buildUrl('logout'),
silent_redirect_uri: apiUrl.buildUrl('client-callback-silent'),
popup_redirect_uri: apiUrl.buildUrl('client-callback-popup'),
authority: apiUrl.buildUrl('identity-server/'),
userStore: new WebStorageStateStore({ store: window.localStorage || window.sessionStorage }),
client_id: 'squidex-frontend',
scope: 'squidex-api openid profile email squidex-profile role permissions',
response_type: 'id_token token',
redirect_uri: apiUrl.buildUrl('login;'),
post_logout_redirect_uri: apiUrl.buildUrl('logout'),
silent_redirect_uri: apiUrl.buildUrl('client-callback-silent'),
popup_redirect_uri: apiUrl.buildUrl('client-callback-popup'),
authority: apiUrl.buildUrl('identity-server/'),
userStore: new WebStorageStateStore({ store: window.localStorage || window.sessionStorage }),
automaticSilentRenew: true
});
@ -103,7 +103,7 @@ export class AuthService {
}
public logoutRedirect() {
this.userManager.signoutRedirect();
this.userManager.signoutRedirect();
}
public loginRedirect() {

60
src/Squidex/app/shared/services/backups.service.ts

@ -85,51 +85,51 @@ export class BackupsService {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/backups`);
return this.http.get<{ items: any[], _links: {} } & Resource>(url).pipe(
map(({ items, _links }) => {
const backups = items.map(item => parseBackup(item));
map(({ items, _links }) => {
const backups = items.map(item => parseBackup(item));
return new BackupsDto(backups, _links);
}),
pretifyError('Failed to load backups.'));
return new BackupsDto(backups, _links);
}),
pretifyError('Failed to load backups.'));
}
public getRestore(): Observable<RestoreDto | null> {
const url = this.apiUrl.buildUrl(`api/apps/restore`);
return this.http.get(url).pipe(
map(body => {
const restore = parseRestore(body);
return restore;
}),
catchError(error => {
if (Types.is(error, HttpErrorResponse) && error.status === 404) {
return of(null);
} else {
return throwError(error);
}
}),
pretifyError('Failed to load backups.'));
map(body => {
const restore = parseRestore(body);
return restore;
}),
catchError(error => {
if (Types.is(error, HttpErrorResponse) && error.status === 404) {
return of(null);
} else {
return throwError(error);
}
}),
pretifyError('Failed to load backups.'));
}
public postBackup(appName: string): Observable<any> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/backups`);
return this.http.post(url, {}).pipe(
tap(() => {
this.analytics.trackEvent('Backup', 'Started', appName);
}),
pretifyError('Failed to start backup.'));
tap(() => {
this.analytics.trackEvent('Backup', 'Started', appName);
}),
pretifyError('Failed to start backup.'));
}
public postRestore(dto: StartRestoreDto): Observable<any> {
const url = this.apiUrl.buildUrl(`api/apps/restore`);
return this.http.post(url, dto).pipe(
tap(() => {
this.analytics.trackEvent('Restore', 'Started');
}),
pretifyError('Failed to start restore.'));
tap(() => {
this.analytics.trackEvent('Restore', 'Started');
}),
pretifyError('Failed to start restore.'));
}
public deleteBackup(appName: string, resource: Resource): Observable<any> {
@ -138,10 +138,10 @@ export class BackupsService {
const url = this.apiUrl.buildUrl(link.href);
return this.http.request(link.method, url).pipe(
tap(() => {
this.analytics.trackEvent('Backup', 'Deleted', appName);
}),
pretifyError('Failed to delete backup.'));
tap(() => {
this.analytics.trackEvent('Backup', 'Deleted', appName);
}),
pretifyError('Failed to delete backup.'));
}
}

58
src/Squidex/app/shared/services/clients.service.ts

@ -80,23 +80,23 @@ export class ClientsService {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/clients`);
return HTTP.getVersioned(this.http, url).pipe(
mapVersioned(({ body }) => {
return parseClients(body);
}),
pretifyError('Failed to load clients. Please reload.'));
mapVersioned(({ body }) => {
return parseClients(body);
}),
pretifyError('Failed to load clients. Please reload.'));
}
public postClient(appName: string, dto: CreateClientDto, version: Version): Observable<ClientsDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/clients`);
return HTTP.postVersioned(this.http, url, dto, version).pipe(
mapVersioned(({ body }) => {
return parseClients(body);
}),
tap(() => {
this.analytics.trackEvent('Client', 'Created', appName);
}),
pretifyError('Failed to add client. Please reload.'));
mapVersioned(({ body }) => {
return parseClients(body);
}),
tap(() => {
this.analytics.trackEvent('Client', 'Created', appName);
}),
pretifyError('Failed to add client. Please reload.'));
}
public putClient(appName: string, resource: Resource, dto: UpdateClientDto, version: Version): Observable<ClientsDto> {
@ -105,13 +105,13 @@ export class ClientsService {
const url = this.apiUrl.buildUrl(link.href);
return HTTP.requestVersioned(this.http, link.method, url, version, dto).pipe(
mapVersioned(({ body }) => {
return parseClients(body);
}),
tap(() => {
this.analytics.trackEvent('Client', 'Updated', appName);
}),
pretifyError('Failed to revoke client. Please reload.'));
mapVersioned(({ body }) => {
return parseClients(body);
}),
tap(() => {
this.analytics.trackEvent('Client', 'Updated', appName);
}),
pretifyError('Failed to revoke client. Please reload.'));
}
public deleteClient(appName: string, resource: Resource, version: Version): Observable<ClientsDto> {
@ -120,13 +120,13 @@ export class ClientsService {
const url = this.apiUrl.buildUrl(link.href);
return HTTP.requestVersioned(this.http, link.method, url, version).pipe(
mapVersioned(({ body }) => {
return parseClients(body);
}),
tap(() => {
this.analytics.trackEvent('Client', 'Deleted', appName);
}),
pretifyError('Failed to revoke client. Please reload.'));
mapVersioned(({ body }) => {
return parseClients(body);
}),
tap(() => {
this.analytics.trackEvent('Client', 'Deleted', appName);
}),
pretifyError('Failed to revoke client. Please reload.'));
}
public createToken(appName: string, client: ClientDto): Observable<AccessTokenDto> {
@ -141,10 +141,10 @@ export class ClientsService {
const url = this.apiUrl.buildUrl('identity-server/connect/token');
return this.http.post(url, body, options).pipe(
map((response: any) => {
return new AccessTokenDto(response.access_token, response.token_type);
}),
pretifyError('Failed to create token. Please retry.'));
map((response: any) => {
return new AccessTokenDto(response.access_token, response.token_type);
}),
pretifyError('Failed to create token. Please retry.'));
}
}

70
src/Squidex/app/shared/services/comments.service.ts

@ -56,58 +56,58 @@ export class CommentsService {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/comments/${commentsId}?version=${version.value}`);
return this.http.get<any>(url).pipe(
map(body => {
const comments = new CommentsDto(
body.createdComments.map((item: any) => {
return new CommentDto(
item.id,
DateTime.parseISO_UTC(item.time),
item.text,
item.user);
}),
body.updatedComments.map((item: any) => {
return new CommentDto(
item.id,
DateTime.parseISO_UTC(item.time),
item.text,
item.user);
}),
body.deletedComments,
new Version(body.version)
);
return comments;
}),
pretifyError('Failed to load comments.'));
map(body => {
const comments = new CommentsDto(
body.createdComments.map((item: any) => {
return new CommentDto(
item.id,
DateTime.parseISO_UTC(item.time),
item.text,
item.user);
}),
body.updatedComments.map((item: any) => {
return new CommentDto(
item.id,
DateTime.parseISO_UTC(item.time),
item.text,
item.user);
}),
body.deletedComments,
new Version(body.version)
);
return comments;
}),
pretifyError('Failed to load comments.'));
}
public postComment(appName: string, commentsId: string, dto: UpsertCommentDto): Observable<CommentDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/comments/${commentsId}`);
return this.http.post<any>(url, dto).pipe(
map(body => {
const comment = new CommentDto(
body.id,
DateTime.parseISO_UTC(body.time),
body.text,
body.user);
return comment;
}),
pretifyError('Failed to create comment.'));
map(body => {
const comment = new CommentDto(
body.id,
DateTime.parseISO_UTC(body.time),
body.text,
body.user);
return comment;
}),
pretifyError('Failed to create comment.'));
}
public putComment(appName: string, commentsId: string, commentId: string, dto: UpsertCommentDto): Observable<any> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/comments/${commentsId}/${commentId}`);
return this.http.put(url, dto).pipe(
pretifyError('Failed to update comment.'));
pretifyError('Failed to update comment.'));
}
public deleteComment(appName: string, commentsId: string, commentId: string): Observable<any> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/comments/${commentsId}/${commentId}`);
return this.http.delete(url).pipe(
pretifyError('Failed to delete comment.'));
pretifyError('Failed to delete comment.'));
}
}

132
src/Squidex/app/shared/services/contents.service.ts

@ -137,45 +137,45 @@ export class ContentsService {
const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}?${fullQuery}`);
return this.http.get<{ total: number, items: [], statuses: StatusInfo[] } & Resource>(url).pipe(
map(({ total, items, statuses, _links }) => {
const contents = items.map(x => parseContent(x));
map(({ total, items, statuses, _links }) => {
const contents = items.map(x => parseContent(x));
return new ContentsDto(statuses, total, contents, _links);
}),
pretifyError('Failed to load contents. Please reload.'));
return new ContentsDto(statuses, total, contents, _links);
}),
pretifyError('Failed to load contents. Please reload.'));
}
public getContent(appName: string, schemaName: string, id: string): Observable<ContentDto> {
const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}`);
return HTTP.getVersioned(this.http, url).pipe(
map(({ payload }) => {
return parseContent(payload.body);
}),
pretifyError('Failed to load content. Please reload.'));
map(({ payload }) => {
return parseContent(payload.body);
}),
pretifyError('Failed to load content. Please reload.'));
}
public getVersionData(appName: string, schemaName: string, id: string, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}/${version.value}`);
return HTTP.getVersioned(this.http, url).pipe(
mapVersioned(({ body }) => {
return body;
}),
pretifyError('Failed to load data. Please reload.'));
mapVersioned(({ body }) => {
return body;
}),
pretifyError('Failed to load data. Please reload.'));
}
public postContent(appName: string, schemaName: string, dto: any, publish: boolean): Observable<ContentDto> {
const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}?publish=${publish}`);
return HTTP.postVersioned(this.http, url, dto).pipe(
map(({ payload }) => {
return parseContent(payload.body);
}),
tap(() => {
this.analytics.trackEvent('Content', 'Created', appName);
}),
pretifyError('Failed to create content. Please reload.'));
map(({ payload }) => {
return parseContent(payload.body);
}),
tap(() => {
this.analytics.trackEvent('Content', 'Created', appName);
}),
pretifyError('Failed to create content. Please reload.'));
}
public putContent(appName: string, resource: Resource, dto: any, version: Version): Observable<ContentDto> {
@ -184,13 +184,13 @@ export class ContentsService {
const url = this.apiUrl.buildUrl(link.href);
return HTTP.putVersioned(this.http, url, dto, version).pipe(
map(({ payload }) => {
return parseContent(payload.body);
}),
tap(() => {
this.analytics.trackEvent('Content', 'Updated', appName);
}),
pretifyError('Failed to update content. Please reload.'));
map(({ payload }) => {
return parseContent(payload.body);
}),
tap(() => {
this.analytics.trackEvent('Content', 'Updated', appName);
}),
pretifyError('Failed to update content. Please reload.'));
}
public patchContent(appName: string, resource: Resource, dto: any, version: Version): Observable<ContentDto> {
@ -199,13 +199,13 @@ export class ContentsService {
const url = this.apiUrl.buildUrl(link.href);
return HTTP.requestVersioned(this.http, link.method, url, version, dto).pipe(
map(({ payload }) => {
return parseContent(payload.body);
}),
tap(() => {
this.analytics.trackEvent('Content', 'Updated', appName);
}),
pretifyError('Failed to update content. Please reload.'));
map(({ payload }) => {
return parseContent(payload.body);
}),
tap(() => {
this.analytics.trackEvent('Content', 'Updated', appName);
}),
pretifyError('Failed to update content. Please reload.'));
}
public discardDraft(appName: string, resource: Resource, version: Version): Observable<ContentDto> {
@ -214,13 +214,13 @@ export class ContentsService {
const url = this.apiUrl.buildUrl(link.href);
return HTTP.requestVersioned(this.http, link.method, url, version, {}).pipe(
map(({ payload }) => {
return parseContent(payload.body);
}),
tap(() => {
this.analytics.trackEvent('Content', 'Discarded', appName);
}),
pretifyError('Failed to discard draft. Please reload.'));
map(({ payload }) => {
return parseContent(payload.body);
}),
tap(() => {
this.analytics.trackEvent('Content', 'Discarded', appName);
}),
pretifyError('Failed to discard draft. Please reload.'));
}
public proposeDraft(appName: string, resource: Resource, dto: any, version: Version): Observable<ContentDto> {
@ -229,13 +229,13 @@ export class ContentsService {
const url = this.apiUrl.buildUrl(link.href);
return HTTP.putVersioned(this.http, url, dto, version).pipe(
map(({ payload }) => {
return parseContent(payload.body);
}),
tap(() => {
this.analytics.trackEvent('Content', 'Updated', appName);
}),
pretifyError('Failed to propose draft. Please reload.'));
map(({ payload }) => {
return parseContent(payload.body);
}),
tap(() => {
this.analytics.trackEvent('Content', 'Updated', appName);
}),
pretifyError('Failed to propose draft. Please reload.'));
}
public publishDraft(appName: string, resource: Resource, dueTime: string | null, version: Version): Observable<ContentDto> {
@ -244,13 +244,13 @@ export class ContentsService {
const url = this.apiUrl.buildUrl(link.href);
return HTTP.requestVersioned(this.http, link.method, url, version, { status: 'Published', dueTime }).pipe(
map(({ payload }) => {
return parseContent(payload.body);
}),
tap(() => {
this.analytics.trackEvent('Content', 'Discarded', appName);
}),
pretifyError('Failed to publish draft. Please reload.'));
map(({ payload }) => {
return parseContent(payload.body);
}),
tap(() => {
this.analytics.trackEvent('Content', 'Discarded', appName);
}),
pretifyError('Failed to publish draft. Please reload.'));
}
public putStatus(appName: string, resource: Resource, status: string, dueTime: string | null, version: Version): Observable<ContentDto> {
@ -259,13 +259,13 @@ export class ContentsService {
const url = this.apiUrl.buildUrl(link.href);
return HTTP.requestVersioned(this.http, link.method, url, version, { status, dueTime }).pipe(
map(({ payload }) => {
return parseContent(payload.body);
}),
tap(() => {
this.analytics.trackEvent('Content', 'Archived', appName);
}),
pretifyError(`Failed to ${status} content. Please reload.`));
map(({ payload }) => {
return parseContent(payload.body);
}),
tap(() => {
this.analytics.trackEvent('Content', 'Archived', appName);
}),
pretifyError(`Failed to ${status} content. Please reload.`));
}
public deleteContent(appName: string, resource: Resource, version: Version): Observable<Versioned<any>> {
@ -274,10 +274,10 @@ export class ContentsService {
const url = this.apiUrl.buildUrl(link.href);
return HTTP.requestVersioned(this.http, link.method, url, version).pipe(
tap(() => {
this.analytics.trackEvent('Content', 'Deleted', appName);
}),
pretifyError('Failed to delete content. Please reload.'));
tap(() => {
this.analytics.trackEvent('Content', 'Deleted', appName);
}),
pretifyError('Failed to delete content. Please reload.'));
}
}

42
src/Squidex/app/shared/services/contributors.service.ts

@ -50,7 +50,7 @@ export class ContributorDto {
}
}
export interface AssignContributorDto {
export interface AssignContributorDto {
readonly contributorId: string;
readonly role: string;
readonly invite?: boolean;
@ -69,23 +69,23 @@ export class ContributorsService {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/contributors`);
return HTTP.getVersioned(this.http, url).pipe(
mapVersioned(({ body }) => {
return parseContributors(body);
}),
pretifyError('Failed to load contributors. Please reload.'));
mapVersioned(({ body }) => {
return parseContributors(body);
}),
pretifyError('Failed to load contributors. Please reload.'));
}
public postContributor(appName: string, dto: AssignContributorDto, version: Version): Observable<ContributorsDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/contributors`);
return HTTP.postVersioned(this.http, url, dto, version).pipe(
mapVersioned(({ body }) => {
return parseContributors(body);
}),
tap(() => {
this.analytics.trackEvent('Contributor', 'Configured', appName);
}),
pretifyError('Failed to add contributors. Please reload.'));
mapVersioned(({ body }) => {
return parseContributors(body);
}),
tap(() => {
this.analytics.trackEvent('Contributor', 'Configured', appName);
}),
pretifyError('Failed to add contributors. Please reload.'));
}
public deleteContributor(appName: string, resource: Resource, version: Version): Observable<ContributorsDto> {
@ -94,15 +94,15 @@ export class ContributorsService {
const url = this.apiUrl.buildUrl(link.href);
return HTTP.requestVersioned(this.http, link.method, url, version).pipe(
mapVersioned(payload => {
const body = payload.body;
return parseContributors(body);
}),
tap(() => {
this.analytics.trackEvent('Contributor', 'Deleted', appName);
}),
pretifyError('Failed to delete contributors. Please reload.'));
mapVersioned(payload => {
const body = payload.body;
return parseContributors(body);
}),
tap(() => {
this.analytics.trackEvent('Contributor', 'Deleted', appName);
}),
pretifyError('Failed to delete contributors. Please reload.'));
}
}

2
src/Squidex/app/shared/services/help.service.ts

@ -21,6 +21,6 @@ export class HelpService {
const url = `https://raw.githubusercontent.com/Squidex/squidex-docs/master/${helpPage}.md`;
return this.http.get(url, { responseType: 'text' }).pipe(
catchError(() => of('')));
catchError(() => of('')));
}
}

24
src/Squidex/app/shared/services/history.service.ts

@ -82,17 +82,17 @@ export class HistoryService {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/history?channel=${channel}`);
return this.http.get<any[]>(url).pipe(
map(body => {
const history = body.map(item =>
new HistoryEventDto(
item.eventId,
item.actor,
item.message,
item.version,
DateTime.parseISO_UTC(item.created)));
return history;
}),
pretifyError('Failed to load history. Please reload.'));
map(body => {
const history = body.map(item =>
new HistoryEventDto(
item.eventId,
item.actor,
item.message,
item.version,
DateTime.parseISO_UTC(item.created)));
return history;
}),
pretifyError('Failed to load history. Please reload.'));
}
}

16
src/Squidex/app/shared/services/languages.service.ts

@ -32,14 +32,14 @@ export class LanguagesService {
const url = this.apiUrl.buildUrl('api/languages');
return this.http.get<any[]>(url).pipe(
map(body => {
const languages = body.map(item =>
new LanguageDto(
item.iso2Code,
item.englishName));
map(body => {
const languages = body.map(item =>
new LanguageDto(
item.iso2Code,
item.englishName));
return languages;
}),
pretifyError('Failed to load languages. Please reload.'));
return languages;
}),
pretifyError('Failed to load languages. Please reload.'));
}
}

28
src/Squidex/app/shared/services/news.service.ts

@ -40,19 +40,19 @@ export class NewsService {
const url = this.apiUrl.buildUrl(`api/news/features?version=${version}`);
return this.http.get<any>(url).pipe(
map(body => {
const items: any[] = body.features;
const features = new FeaturesDto(
items.map(item =>
new FeatureDto(
item.name,
item.text)
),
body.version);
return features;
}),
pretifyError('Failed to load features. Please reload.'));
map(body => {
const items: any[] = body.features;
const features = new FeaturesDto(
items.map(item =>
new FeatureDto(
item.name,
item.text)
),
body.version);
return features;
}),
pretifyError('Failed to load features. Please reload.'));
}
}

56
src/Squidex/app/shared/services/plans.service.ts

@ -62,42 +62,42 @@ export class PlansService {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/plans`);
return HTTP.getVersioned(this.http, url).pipe(
mapVersioned(({ body }) => {
const items: any[] = body.plans;
mapVersioned(({ body }) => {
const items: any[] = body.plans;
const { hasPortal, currentPlanId, planOwner } = body;
const { hasPortal, currentPlanId, planOwner } = body;
const plans = {
currentPlanId,
planOwner,
plans: items.map(item =>
new PlanDto(
item.id,
item.name,
item.costs,
item.yearlyId,
item.yearlyCosts,
item.maxApiCalls,
item.maxAssetSize,
item.maxContributors)),
hasPortal
};
const plans = {
currentPlanId,
planOwner,
plans: items.map(item =>
new PlanDto(
item.id,
item.name,
item.costs,
item.yearlyId,
item.yearlyCosts,
item.maxApiCalls,
item.maxAssetSize,
item.maxContributors)),
hasPortal
};
return plans;
}),
pretifyError('Failed to load plans. Please reload.'));
return plans;
}),
pretifyError('Failed to load plans. Please reload.'));
}
public putPlan(appName: string, dto: ChangePlanDto, version: Version): Observable<Versioned<PlanChangedDto>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/plan`);
return HTTP.putVersioned(this.http, url, dto, version).pipe(
mapVersioned(payload => {
return <PlanChangedDto>payload.body;
}),
tap(() => {
this.analytics.trackEvent('Plan', 'Changed', appName);
}),
pretifyError('Failed to change plan. Please reload.'));
mapVersioned(payload => {
return <PlanChangedDto>payload.body;
}),
tap(() => {
this.analytics.trackEvent('Plan', 'Changed', appName);
}),
pretifyError('Failed to change plan. Please reload.'));
}
}

52
src/Squidex/app/shared/services/roles.service.ts

@ -72,23 +72,23 @@ export class RolesService {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/roles`);
return HTTP.getVersioned(this.http, url).pipe(
mapVersioned(({ body }) => {
return parseRoles(body);
}),
pretifyError('Failed to load roles. Please reload.'));
mapVersioned(({ body }) => {
return parseRoles(body);
}),
pretifyError('Failed to load roles. Please reload.'));
}
public postRole(appName: string, dto: CreateRoleDto, version: Version): Observable<RolesDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/roles`);
return HTTP.postVersioned(this.http, url, dto, version).pipe(
mapVersioned(({ body }) => {
return parseRoles(body);
}),
tap(() => {
this.analytics.trackEvent('Role', 'Created', appName);
}),
pretifyError('Failed to add role. Please reload.'));
mapVersioned(({ body }) => {
return parseRoles(body);
}),
tap(() => {
this.analytics.trackEvent('Role', 'Created', appName);
}),
pretifyError('Failed to add role. Please reload.'));
}
public putRole(appName: string, resource: Resource, dto: UpdateRoleDto, version: Version): Observable<RolesDto> {
@ -97,13 +97,13 @@ export class RolesService {
const url = this.apiUrl.buildUrl(link.href);
return HTTP.requestVersioned(this.http, link.method, url, version, dto).pipe(
mapVersioned(({ body }) => {
return parseRoles(body);
}),
tap(() => {
this.analytics.trackEvent('Role', 'Updated', appName);
}),
pretifyError('Failed to revoke role. Please reload.'));
mapVersioned(({ body }) => {
return parseRoles(body);
}),
tap(() => {
this.analytics.trackEvent('Role', 'Updated', appName);
}),
pretifyError('Failed to revoke role. Please reload.'));
}
public deleteRole(appName: string, resource: Resource, version: Version): Observable<RolesDto> {
@ -112,20 +112,20 @@ export class RolesService {
const url = this.apiUrl.buildUrl(link.href);
return HTTP.requestVersioned(this.http, link.method, url, version).pipe(
mapVersioned(({ body }) => {
return parseRoles(body);
}),
tap(() => {
this.analytics.trackEvent('Role', 'Deleted', appName);
}),
pretifyError('Failed to revoke role. Please reload.'));
mapVersioned(({ body }) => {
return parseRoles(body);
}),
tap(() => {
this.analytics.trackEvent('Role', 'Deleted', appName);
}),
pretifyError('Failed to revoke role. Please reload.'));
}
public getPermissions(appName: string): Observable<string[]> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/roles/permissions`);
return this.http.get<string[]>(url).pipe(
pretifyError('Failed to load permissions. Please reload.'));
pretifyError('Failed to load permissions. Please reload.'));
}
}

3
src/Squidex/app/shared/services/rules.service.ts

@ -41,7 +41,8 @@ export const ALL_TRIGGERS = {
description: 'When a schema definition has been created, updated, published or deleted...',
display: 'Schema changed',
iconColor: '#3389ff',
iconCode: 'schemas'},
iconCode: 'schemas'
},
'Usage': {
description: 'When monthly API calls exceed a specified limit for one time a month...',
display: 'Usage exceeded',

8
src/Squidex/app/shared/services/translations.service.ts

@ -38,9 +38,9 @@ export class TranslationsService {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/translations`);
return this.http.post<any>(url, request).pipe(
map(body => {
return new TranslationDto(body.result, body.text);
}),
pretifyError('Failed to translate text. Please reload.'));
map(body => {
return new TranslationDto(body.result, body.text);
}),
pretifyError('Failed to translate text. Please reload.'));
}
}

62
src/Squidex/app/shared/services/usages.service.ts

@ -72,55 +72,55 @@ export class UsagesService {
const url = this.apiUrl.buildUrl(`api/apps/${app}/usages/calls/month`);
return this.http.get<any>(url).pipe(
map(body => {
return new CurrentCallsDto(body.count, body.maxAllowed);
}),
pretifyError('Failed to load monthly api calls. Please reload.'));
map(body => {
return new CurrentCallsDto(body.count, body.maxAllowed);
}),
pretifyError('Failed to load monthly api calls. Please reload.'));
}
public getTodayStorage(app: string): Observable<CurrentStorageDto> {
const url = this.apiUrl.buildUrl(`api/apps/${app}/usages/storage/today`);
return this.http.get<any>(url).pipe(
map(body => {
return new CurrentStorageDto(body.size, body.maxAllowed);
}),
pretifyError('Failed to load todays storage size. Please reload.'));
map(body => {
return new CurrentStorageDto(body.size, body.maxAllowed);
}),
pretifyError('Failed to load todays storage size. Please reload.'));
}
public getCallsUsages(app: string, fromDate: DateTime, toDate: DateTime): Observable<{ [category: string]: CallsUsageDto[] }> {
const url = this.apiUrl.buildUrl(`api/apps/${app}/usages/calls/${fromDate.toUTCStringFormat('YYYY-MM-DD')}/${toDate.toUTCStringFormat('YYYY-MM-DD')}`);
return this.http.get<any>(url).pipe(
map(body => {
const usages: { [category: string]: CallsUsageDto[] } = {};
for (let category of Object.keys(body)) {
usages[category] = body[category].map((item: any) =>
new CallsUsageDto(
DateTime.parseISO_UTC(item.date),
item.count,
item.averageMs));
}
return usages;
}),
pretifyError('Failed to load calls usage. Please reload.'));
map(body => {
const usages: { [category: string]: CallsUsageDto[] } = {};
for (let category of Object.keys(body)) {
usages[category] = body[category].map((item: any) =>
new CallsUsageDto(
DateTime.parseISO_UTC(item.date),
item.count,
item.averageMs));
}
return usages;
}),
pretifyError('Failed to load calls usage. Please reload.'));
}
public getStorageUsages(app: string, fromDate: DateTime, toDate: DateTime): Observable<StorageUsageDto[]> {
const url = this.apiUrl.buildUrl(`api/apps/${app}/usages/storage/${fromDate.toUTCStringFormat('YYYY-MM-DD')}/${toDate.toUTCStringFormat('YYYY-MM-DD')}`);
return this.http.get<any[]>(url).pipe(
map(body => {
const usages = body.map(item =>
new StorageUsageDto(
DateTime.parseISO_UTC(item.date),
item.count,
item.size));
map(body => {
const usages = body.map(item =>
new StorageUsageDto(
DateTime.parseISO_UTC(item.date),
item.count,
item.size));
return usages;
}),
pretifyError('Failed to load storage usage. Please reload.'));
return usages;
}),
pretifyError('Failed to load storage usage. Please reload.'));
}
}

4
tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetQueryServiceTests.cs

@ -30,7 +30,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
private readonly IAssetRepository assetRepository = A.Fake<IAssetRepository>();
private readonly IAppEntity app = A.Fake<IAppEntity>();
private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app");
private readonly QueryContext context;
private readonly Context context;
private readonly AssetQueryService sut;
public AssetQueryServiceTests()
@ -41,7 +41,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
A.CallTo(() => app.Name).Returns(appId.Name);
A.CallTo(() => app.LanguagesConfig).Returns(LanguagesConfig.English);
context = QueryContext.Create(app, user);
context = new Context(user, app);
var options = Options.Create(new AssetOptions { DefaultPageSize = 30 });

36
tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentEnricherTests.cs

@ -16,13 +16,18 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
public class ContentEnricherTests
{
private readonly IContentWorkflow workflow = A.Fake<IContentWorkflow>();
private readonly IContentWorkflow contentWorkflow = A.Fake<IContentWorkflow>();
private readonly IContextProvider contextProvider = A.Fake<IContextProvider>();
private readonly Context context = new Context();
private readonly NamedId<Guid> schemaId = NamedId.Of(Guid.NewGuid(), "my-schema");
private readonly ContentEnricher sut;
public ContentEnricherTests()
{
sut = new ContentEnricher(workflow);
A.CallTo(() => contextProvider.Context)
.Returns(context);
sut = new ContentEnricher(contentWorkflow, contextProvider);
}
[Fact]
@ -30,7 +35,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
var source = new ContentEntity { Status = Status.Published, SchemaId = schemaId };
A.CallTo(() => workflow.GetInfoAsync(source))
A.CallTo(() => contentWorkflow.GetInfoAsync(source))
.Returns(new StatusInfo(Status.Published, StatusColors.Published));
var result = await sut.EnrichAsync(source);
@ -43,7 +48,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
var source = new ContentEntity { Status = Status.Published, SchemaId = schemaId };
A.CallTo(() => workflow.GetInfoAsync(source))
A.CallTo(() => contentWorkflow.GetInfoAsync(source))
.Returns(Task.FromResult<StatusInfo>(null));
var result = await sut.EnrichAsync(source);
@ -54,9 +59,11 @@ namespace Squidex.Domain.Apps.Entities.Contents
[Fact]
public async Task Should_enrich_content_with_can_update()
{
context.WithResolveFlow(true);
var source = new ContentEntity { SchemaId = schemaId };
A.CallTo(() => workflow.CanUpdateAsync(source))
A.CallTo(() => contentWorkflow.CanUpdateAsync(source))
.Returns(true);
var result = await sut.EnrichAsync(source);
@ -64,13 +71,28 @@ namespace Squidex.Domain.Apps.Entities.Contents
Assert.True(result.CanUpdate);
}
[Fact]
public async Task Should_not_enrich_content_with_can_update_if_disabled_in_context()
{
context.WithResolveFlow(false);
var source = new ContentEntity { SchemaId = schemaId };
var result = await sut.EnrichAsync(source);
Assert.False(result.CanUpdate);
A.CallTo(() => contentWorkflow.CanUpdateAsync(source))
.MustNotHaveHappened();
}
[Fact]
public async Task Should_enrich_multiple_contents_and_cache_color()
{
var source1 = new ContentEntity { Status = Status.Published, SchemaId = schemaId };
var source2 = new ContentEntity { Status = Status.Published, SchemaId = schemaId };
A.CallTo(() => workflow.GetInfoAsync(source1))
A.CallTo(() => contentWorkflow.GetInfoAsync(source1))
.Returns(new StatusInfo(Status.Published, StatusColors.Published));
var result = await sut.EnrichAsync(new[] { source1, source2 });
@ -78,7 +100,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
Assert.Equal(StatusColors.Published, result[0].StatusColor);
Assert.Equal(StatusColors.Published, result[1].StatusColor);
A.CallTo(() => workflow.GetInfoAsync(A<IContentEntity>.Ignored))
A.CallTo(() => contentWorkflow.GetInfoAsync(A<IContentEntity>.Ignored))
.MustHaveHappenedOnceExactly();
}
}

32
tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs

@ -53,13 +53,13 @@ namespace Squidex.Domain.Apps.Entities.Contents
private readonly ClaimsPrincipal user;
private readonly ClaimsIdentity identity = new ClaimsIdentity();
private readonly EdmModelBuilder modelBuilder = new EdmModelBuilder(new MemoryCache(Options.Create(new MemoryCacheOptions())));
private readonly QueryContext context;
private readonly Context context;
private readonly ContentQueryService sut;
public static IEnumerable<object[]> ApiStatusTests = new[]
{
new object[] { StatusForApi.PublishedOnly, 0, new[] { Status.Published } },
new object[] { StatusForApi.All, 1, null }
new object[] { 0, new[] { Status.Published } },
new object[] { 1, null }
};
public ContentQueryServiceTests()
@ -80,7 +80,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
SetupEnricher();
context = QueryContext.Create(app, user);
context = new Context(user, app);
var options = Options.Create(new ContentOptions { DefaultPageSize = 30 });
@ -221,16 +221,16 @@ namespace Squidex.Domain.Apps.Entities.Contents
[Theory]
[MemberData(nameof(ApiStatusTests))]
public async Task Should_return_single_content_for_api_with_transform(StatusForApi request, int includeDraft, Status[] status)
public async Task Should_return_single_content_for_api_with_transform(int unpublished, Status[] status)
{
var content = CreateContent(contentId);
SetupClaims(isFrontend: false);
SetupSchemaFound();
SetupScripting(contentId);
SetupContent(status, content, includeDraft == 1);
SetupContent(status, content, unpublished == 1);
var ctx = context.WithApiStatus(request);
var ctx = context.WithUnpublished(unpublished == 1);
var result = await sut.FindContentAsync(ctx, schemaId.Name, contentId);
@ -299,7 +299,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
[Theory]
[MemberData(nameof(ApiStatusTests))]
public async Task Should_query_contents_by_query_for_api_and_transform(StatusForApi request, int includeDraft, Status[] status)
public async Task Should_query_contents_by_query_for_api_and_transform(int unpublished, Status[] status)
{
const int count = 5, total = 200;
@ -308,9 +308,9 @@ namespace Squidex.Domain.Apps.Entities.Contents
SetupClaims(isFrontend: false);
SetupSchemaFound();
SetupScripting(contentId);
SetupContents(status, count, total, content, inDraft: false, includeDraft == 1);
SetupContents(status, count, total, content, inDraft: false, unpublished == 1);
var ctx = context.WithApiStatus(request);
var ctx = context.WithUnpublished(unpublished == 1);
var result = await sut.QueryAsync(ctx, schemaId.Name, Q.Empty);
@ -359,7 +359,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
[Theory]
[MemberData(nameof(ApiStatusTests))]
public async Task Should_query_contents_by_id_for_api_and_transform(StatusForApi request, int includeDraft, Status[] status)
public async Task Should_query_contents_by_id_for_api_and_transform(int unpublished, Status[] status)
{
const int count = 5, total = 200;
@ -368,9 +368,9 @@ namespace Squidex.Domain.Apps.Entities.Contents
SetupClaims(isFrontend: false);
SetupSchemaFound();
SetupScripting(ids.ToArray());
SetupContents(status, total, ids, includeDraft == 1);
SetupContents(status, total, ids, unpublished == 1);
var ctx = context.WithApiStatus(request);
var ctx = context.WithUnpublished(unpublished == 1);
var result = await sut.QueryAsync(ctx, schemaId.Name, Q.Empty.WithIds(ids));
@ -405,7 +405,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
[Theory]
[MemberData(nameof(ApiStatusTests))]
public async Task Should_query_all_contents_by_id_for_api_and_transform(StatusForApi request, int includeDraft, Status[] status)
public async Task Should_query_all_contents_by_id_for_api_and_transform(int unpublished, Status[] status)
{
const int count = 5;
@ -414,9 +414,9 @@ namespace Squidex.Domain.Apps.Entities.Contents
SetupClaims(isFrontend: false);
SetupSchemaFound();
SetupScripting(ids.ToArray());
SetupContents(status, ids, includeDraft == 1);
SetupContents(status, ids, unpublished == 1);
var ctx = context.WithApiStatus(request);
var ctx = context.WithUnpublished(unpublished == 1);
var result = await sut.QueryAsync(ctx, ids);

8
tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs

@ -1017,14 +1017,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
return A<Q>.That.Matches(x => x.Ids.Count == 1 && x.Ids[0] == contentId);
}
private QueryContext MatchsAssetContext()
private Context MatchsAssetContext()
{
return A<QueryContext>.That.Matches(x => x.App == app && x.User == user);
return A<Context>.That.Matches(x => x.App == app && x.User == user);
}
private QueryContext MatchsContentContext()
private Context MatchsContentContext()
{
return A<QueryContext>.That.Matches(x => x.App == app && x.User == user);
return A<Context>.That.Matches(x => x.App == app && x.User == user);
}
}
}

4
tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs

@ -45,7 +45,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
protected readonly IJsonSerializer serializer = TestUtils.CreateSerializer(TypeNameHandling.None);
protected readonly IDependencyResolver dependencyResolver;
protected readonly IAppEntity app = A.Dummy<IAppEntity>();
protected readonly QueryContext context;
protected readonly Context context;
protected readonly ClaimsPrincipal user = new ClaimsPrincipal();
protected readonly IGraphQLService sut;
@ -88,7 +88,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
A.CallTo(() => app.Name).Returns(appName);
A.CallTo(() => app.LanguagesConfig).Returns(LanguagesConfig.Build(Language.DE, Language.GermanGermany));
context = QueryContext.Create(app, user);
context = new Context(user, app);
A.CallTo(() => schema.Id).Returns(schemaId);
A.CallTo(() => schema.SchemaDef).Returns(schemaDef);

2
tests/Squidex.Web.Tests/ApiPermissionAttributeTests.cs

@ -38,7 +38,7 @@ namespace Squidex.Web
actionExecutingContext = new ActionExecutingContext(actionContext, new List<IFilterMetadata>(), new Dictionary<string, object>(), this);
actionExecutingContext.HttpContext = httpContext;
actionExecutingContext.HttpContext.User = new ClaimsPrincipal(user);
actionExecutingContext.HttpContext.Context().User = new ClaimsPrincipal(user);
next = () =>
{

31
tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs

@ -8,44 +8,43 @@
using System;
using System.Threading.Tasks;
using FakeItEasy;
using Microsoft.AspNetCore.Http;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Web.Pipeline;
using Xunit;
namespace Squidex.Web.CommandMiddlewares
{
public class EnrichWithAppIdCommandMiddlewareTests
{
private readonly IHttpContextAccessor httpContextAccessor = A.Fake<IHttpContextAccessor>();
private readonly IContextProvider contextProvider = A.Fake<IContextProvider>();
private readonly ICommandBus commandBus = A.Fake<ICommandBus>();
private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app");
private readonly HttpContext httpContext = new DefaultHttpContext();
private readonly Context appContext = new Context();
private readonly EnrichWithAppIdCommandMiddleware sut;
public EnrichWithAppIdCommandMiddlewareTests()
{
A.CallTo(() => httpContextAccessor.HttpContext)
.Returns(httpContext);
A.CallTo(() => contextProvider.Context)
.Returns(appContext);
var app = A.Fake<IAppEntity>();
A.CallTo(() => app.Id).Returns(appId.Id);
A.CallTo(() => app.Name).Returns(appId.Name);
httpContext.Features.Set<IAppFeature>(new AppResolver.AppFeature(app));
appContext.App = app;
sut = new EnrichWithAppIdCommandMiddleware(httpContextAccessor);
sut = new EnrichWithAppIdCommandMiddleware(contextProvider);
}
[Fact]
public async Task Should_throw_exception_if_app_not_found()
{
httpContext.Features.Set<IAppFeature>(new AppResolver.AppFeature(null));
appContext.App = null;
var command = new CreateContent();
var context = new CommandContext(command, commandBus);
@ -53,20 +52,6 @@ namespace Squidex.Web.CommandMiddlewares
await Assert.ThrowsAsync<InvalidOperationException>(() => sut.HandleAsync(context));
}
[Fact]
public async Task Should_do_nothing_when_context_is_null()
{
A.CallTo(() => httpContextAccessor.HttpContext)
.Returns(null);
var command = new CreateContent();
var context = new CommandContext(command, commandBus);
await sut.HandleAsync(context);
Assert.Null(command.Actor);
}
[Fact]
public async Task Should_assign_app_id_and_name_to_app_command()
{

40
tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithSchemaIdCommandMiddlewareTests.cs

@ -20,7 +20,6 @@ using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.Schemas.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Web.Pipeline;
using Xunit;
namespace Squidex.Web.CommandMiddlewares
@ -49,7 +48,7 @@ namespace Squidex.Web.CommandMiddlewares
A.CallTo(() => app.Id).Returns(appId.Id);
A.CallTo(() => app.Name).Returns(appId.Name);
httpContext.Features.Set<IAppFeature>(new AppResolver.AppFeature(app));
httpContext.Context().App = app;
var schema = A.Fake<ISchemaEntity>();
@ -65,33 +64,19 @@ namespace Squidex.Web.CommandMiddlewares
}
[Fact]
public async Task Should_throw_exception_if_app_not_found()
public async Task Should_throw_exception_if_schema_not_found()
{
A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, "other-schema"))
.Returns(Task.FromResult<ISchemaEntity>(null));
actionContext.RouteData.Values["name"] = "other-schema";
var command = new CreateContent();
var command = new CreateContent { AppId = appId };
var context = new CommandContext(command, commandBus);
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.HandleAsync(context));
}
[Fact]
public async Task Should_do_nothing_when_context_is_null()
{
A.CallTo(() => actionContextAccessor.ActionContext)
.Returns(null);
var command = new CreateContent();
var context = new CommandContext(command, commandBus);
await sut.HandleAsync(context);
Assert.Null(command.Actor);
}
[Fact]
public async Task Should_do_nothing_when_route_has_no_parameter()
{
@ -108,7 +93,7 @@ namespace Squidex.Web.CommandMiddlewares
{
actionContext.RouteData.Values["name"] = schemaId.Name;
var command = new CreateContent();
var command = new CreateContent { AppId = appId };
var context = new CommandContext(command, commandBus);
await sut.HandleAsync(context);
@ -119,9 +104,9 @@ namespace Squidex.Web.CommandMiddlewares
[Fact]
public async Task Should_assign_schema_id_and_name_from_id()
{
actionContext.RouteData.Values["name"] = schemaId.Name;
actionContext.RouteData.Values["name"] = schemaId.Id;
var command = new CreateContent();
var command = new CreateContent { AppId = appId };
var context = new CommandContext(command, commandBus);
await sut.HandleAsync(context);
@ -142,19 +127,6 @@ namespace Squidex.Web.CommandMiddlewares
Assert.Equal(schemaId.Id, command.SchemaId);
}
[Fact]
public async Task Should_use_app_id_from_command()
{
actionContext.RouteData.Values["name"] = schemaId.Name;
var command = new CreateContent { AppId = appId };
var context = new CommandContext(command, commandBus);
await sut.HandleAsync(context);
Assert.Equal(schemaId, command.SchemaId);
}
[Fact]
public async Task Should_not_override_schema_id()
{

2
tests/Squidex.Web.Tests/Pipeline/ApiCostsFilterTests.cs

@ -161,7 +161,7 @@ namespace Squidex.Web.Pipeline
private void SetupApp()
{
httpContext.Features.Set<IAppFeature>(new AppResolver.AppFeature(appEntity));
httpContext.Context().App = appEntity;
}
}
}

6
tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs

@ -86,7 +86,7 @@ namespace Squidex.Web.Pipeline
await sut.OnActionExecutionAsync(actionExecutingContext, next);
Assert.Same(app, httpContext.Features.Get<IAppFeature>()?.App);
Assert.Same(app, httpContext.Context().App);
Assert.True(user.Claims.Count() > 2);
Assert.True(isNextCalled);
}
@ -104,7 +104,7 @@ namespace Squidex.Web.Pipeline
await sut.OnActionExecutionAsync(actionExecutingContext, next);
Assert.Same(app, httpContext.Features.Get<IAppFeature>()?.App);
Assert.Same(app, httpContext.Context().App);
Assert.True(user.Claims.Count() > 2);
Assert.True(isNextCalled);
}
@ -124,7 +124,7 @@ namespace Squidex.Web.Pipeline
await sut.OnActionExecutionAsync(actionExecutingContext, next);
Assert.Same(app, httpContext.Features.Get<IAppFeature>()?.App);
Assert.Same(app, httpContext.Context().App);
Assert.Equal(2, user.Claims.Count());
Assert.True(isNextCalled);
}

Loading…
Cancel
Save