Browse Source

Merge pull request #371 from Squidex/hateaos

Hateaos
pull/375/head^2
Sebastian Stehle 7 years ago
committed by GitHub
parent
commit
3095c33c94
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      Squidex.ruleset
  2. 8
      src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs
  3. 46
      src/Squidex.Domain.Apps.Core.Model/Contents/Status2.cs
  4. 5
      src/Squidex.Domain.Apps.Core.Model/Contents/StatusFlow.cs
  5. 4
      src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs
  6. 11
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs
  7. 39
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/StatusSerializer.cs
  8. 4
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterFactory.cs
  9. 58
      src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs
  10. 14
      src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByUserIndexCommandMiddleware.cs
  11. 4
      src/Squidex.Domain.Apps.Entities/Apps/Invitation/InviteUserCommandMiddleware.cs
  12. 4
      src/Squidex.Domain.Apps.Entities/Apps/Invitation/InvitedResult.cs
  13. 2
      src/Squidex.Domain.Apps.Entities/Apps/RoleExtensions.cs
  14. 80
      src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs
  15. 19
      src/Squidex.Domain.Apps.Entities/Assets/AssetCreatedResult.cs
  16. 26
      src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs
  17. 25
      src/Squidex.Domain.Apps.Entities/Assets/AssetResult.cs
  18. 25
      src/Squidex.Domain.Apps.Entities/Assets/AssetSavedResult.cs
  19. 9
      src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs
  20. 9
      src/Squidex.Domain.Apps.Entities/Assets/Commands/UpdateAsset.cs
  21. 20
      src/Squidex.Domain.Apps.Entities/Assets/Commands/UploadAssetCommand.cs
  22. 6
      src/Squidex.Domain.Apps.Entities/Comments/CommentsGrain.cs
  23. 23
      src/Squidex.Domain.Apps.Entities/Contents/ContentDataChangedResult.cs
  24. 26
      src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs
  25. 49
      src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs
  26. 24
      src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs
  27. 2
      src/Squidex.Domain.Apps.Entities/Contents/StatusForApi.cs
  28. 19
      src/Squidex.Domain.Apps.Entities/QueryContext.cs
  29. 18
      src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs
  30. 24
      src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardHelper.cs
  31. 2
      src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs
  32. 50
      src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchemaField.cs
  33. 70
      src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs
  34. 16
      src/Squidex.Domain.Users/UserManagerExtensions.cs
  35. 8
      src/Squidex.Infrastructure/CollectionExtensions.cs
  36. 12
      src/Squidex.Infrastructure/Commands/DomainObjectGrainBase.cs
  37. 29
      src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs
  38. 12
      src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerManagerGrain.cs
  39. 6
      src/Squidex.Infrastructure/EventSourcing/Grains/IEventConsumerGrain.cs
  40. 10
      src/Squidex.Infrastructure/EventSourcing/Grains/IEventConsumerManagerGrain.cs
  41. 10
      src/Squidex.Shared/Permissions.cs
  42. 8
      src/Squidex.Web/Extensions.cs
  43. 76
      src/Squidex.Web/PermissionExtensions.cs
  44. 3
      src/Squidex.Web/Pipeline/AppResolver.cs
  45. 61
      src/Squidex.Web/Resource.cs
  46. 16
      src/Squidex.Web/ResourceLink.cs
  47. 44
      src/Squidex.Web/UrlHelperExtensions.cs
  48. 70
      src/Squidex/Areas/Api/Config/Swagger/ErrorDtoProcessor.cs
  49. 2
      src/Squidex/Areas/Api/Config/Swagger/FixProcessor.cs
  50. 29
      src/Squidex/Areas/Api/Config/Swagger/SecurityProcessor.cs
  51. 3
      src/Squidex/Areas/Api/Config/Swagger/SwaggerServices.cs
  52. 36
      src/Squidex/Areas/Api/Config/Swagger/XmlResponseTypesProcessor.cs
  53. 53
      src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs
  54. 38
      src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs
  55. 42
      src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs
  56. 43
      src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs
  57. 53
      src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs
  58. 19
      src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs
  59. 59
      src/Squidex/Areas/Api/Controllers/Apps/Models/AppCreatedDto.cs
  60. 122
      src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs
  61. 42
      src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguageDto.cs
  62. 53
      src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguagesDto.cs
  63. 29
      src/Squidex/Areas/Api/Controllers/Apps/Models/ClientDto.cs
  64. 48
      src/Squidex/Areas/Api/Controllers/Apps/Models/ClientsDto.cs
  65. 29
      src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorDto.cs
  66. 48
      src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsDto.cs
  67. 17
      src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsMetadata.cs
  68. 30
      src/Squidex/Areas/Api/Controllers/Apps/Models/PatternDto.cs
  69. 48
      src/Squidex/Areas/Api/Controllers/Apps/Models/PatternsDto.cs
  70. 43
      src/Squidex/Areas/Api/Controllers/Apps/Models/RoleDto.cs
  71. 31
      src/Squidex/Areas/Api/Controllers/Apps/Models/RolesDto.cs
  72. 3
      src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateLanguageDto.cs
  73. 34
      src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs
  74. 57
      src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs
  75. 108
      src/Squidex/Areas/Api/Controllers/Assets/Models/AssetCreatedDto.cs
  76. 53
      src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs
  77. 11
      src/Squidex/Areas/Api/Controllers/Assets/Models/AssetMetadata.cs
  78. 74
      src/Squidex/Areas/Api/Controllers/Assets/Models/AssetReplacedDto.cs
  79. 48
      src/Squidex/Areas/Api/Controllers/Assets/Models/AssetsDto.cs
  80. 7
      src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs
  81. 22
      src/Squidex/Areas/Api/Controllers/Backups/Models/BackupJobDto.cs
  82. 49
      src/Squidex/Areas/Api/Controllers/Backups/Models/BackupJobsDto.cs
  83. 4
      src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs
  84. 3
      src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs
  85. 212
      src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs
  86. 88
      src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaSwaggerGenerator.cs
  87. 28
      src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasSwaggerGenerator.cs
  88. 23
      src/Squidex/Areas/Api/Controllers/Contents/Helper.cs
  89. 20
      src/Squidex/Areas/Api/Controllers/Contents/Models/ChangeStatusDto.cs
  90. 84
      src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs
  91. 67
      src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs
  92. 33
      src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs
  93. 34
      src/Squidex/Areas/Api/Controllers/EventConsumers/Models/EventConsumerDto.cs
  94. 39
      src/Squidex/Areas/Api/Controllers/EventConsumers/Models/EventConsumersDto.cs
  95. 4
      src/Squidex/Areas/Api/Controllers/History/HistoryController.cs
  96. 4
      src/Squidex/Areas/Api/Controllers/News/Service/FeaturesService.cs
  97. 3
      src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs
  98. 44
      src/Squidex/Areas/Api/Controllers/Rules/Models/RuleDto.cs
  99. 27
      src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventDto.cs
  100. 20
      src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventsDto.cs

1
Squidex.ruleset

@ -63,6 +63,7 @@
<Rule Id="SA1601" Action="None" />
<Rule Id="SA1413" Action="None" />
<Rule Id="SA0001" Action="None" />
<Rule Id="SA1602" Action="None" />
</Rules>
<Rules AnalyzerId="RefactoringEssentials" RuleNamespace="RefactoringEssentials">
<Rule Id="RECS0061" Action="Error" />

8
src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs

@ -7,6 +7,7 @@
using Squidex.Infrastructure;
using Squidex.Infrastructure.Security;
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using P = Squidex.Shared.Permissions;
@ -20,7 +21,7 @@ namespace Squidex.Domain.Apps.Core.Apps
public const string Owner = "Owner";
public const string Reader = "Reader";
private static readonly HashSet<string> DefaultRolesSet = new HashSet<string>
private static readonly HashSet<string> DefaultRolesSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
Editor,
Developer,
@ -54,6 +55,11 @@ namespace Squidex.Domain.Apps.Core.Apps
return role != null && DefaultRolesSet.Contains(role);
}
public static bool IsRole(string name, string expected)
{
return name != null && string.Equals(name, expected, StringComparison.OrdinalIgnoreCase);
}
public static Role CreateOwner(string app)
{
return new Role(Owner,

46
src/Squidex.Domain.Apps.Core.Model/Contents/Status2.cs

@ -0,0 +1,46 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure;
using System;
namespace Squidex.Domain.Apps.Core.Contents
{
public struct Status2 : IEquatable<Status2>
{
public static readonly Status2 Published = new Status2("Published");
public string Name { get; }
public Status2(string name)
{
Guard.NotNullOrEmpty(name, nameof(name));
Name = name;
}
public override bool Equals(object obj)
{
return obj is Status2 status && Equals(status);
}
public bool Equals(Status2 other)
{
return Name.Equals(other.Name);
}
public override int GetHashCode()
{
return base.GetHashCode();
}
public override string ToString()
{
return Name;
}
}
}

5
src/Squidex.Domain.Apps.Core.Model/Contents/StatusFlow.cs

@ -28,5 +28,10 @@ namespace Squidex.Domain.Apps.Core.Contents
{
return Flow.TryGetValue(status, out var state) && state.Contains(toStatus);
}
public static IEnumerable<Status> Next(Status status)
{
return Flow.TryGetValue(status, out var result) ? result : Enumerable.Empty<Status>();
}
}
}

4
src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs

@ -48,9 +48,9 @@ namespace Squidex.Domain.Apps.Core.Scripting
private static ObjectWrapper CreateUser(Engine engine, string id, bool isClient, string email, string name, IEnumerable<Claim> allClaims)
{
var claims =
allClaims.GroupBy(x => x.Type)
allClaims.GroupBy(x => x.Type.Split(ClaimSeparators).Last())
.ToDictionary(
x => x.Key.Split(ClaimSeparators).Last(),
x => x.Key,
x => x.Select(y => y.Value).ToArray());
return new ObjectWrapper(engine, new { id, isClient, email, name, claims });

11
src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs

@ -36,6 +36,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
private readonly string typeContentDeleted;
private readonly MongoContentCollection contents;
static MongoContentRepository()
{
StatusSerializer.Register();
}
public MongoContentRepository(IMongoDatabase database, IAppProvider appProvider, IJsonSerializer serializer, ITextIndexer indexer, TypeNameRegistry typeNameRegistry)
{
Guard.NotNull(appProvider, nameof(appProvider));
@ -64,7 +69,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{
Guard.NotNull(app, nameof(app));
Guard.NotNull(schema, nameof(schema));
Guard.NotNull(status, nameof(status));
Guard.NotNull(query, nameof(query));
using (Profiler.TraceMethod<MongoContentRepository>("QueryAsyncByQuery"))
@ -83,9 +87,8 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
public async Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet<Guid> ids, bool includeDraft = true)
{
Guard.NotNull(app, nameof(app));
Guard.NotNull(schema, nameof(schema));
Guard.NotNull(status, nameof(status));
Guard.NotNull(ids, nameof(ids));
Guard.NotNull(schema, nameof(schema));
using (Profiler.TraceMethod<MongoContentRepository>("QueryAsyncByIds"))
{
@ -96,7 +99,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
public async Task<List<(IContentEntity Content, ISchemaEntity Schema)>> QueryAsync(IAppEntity app, Status[] status, HashSet<Guid> ids, bool includeDraft = true)
{
Guard.NotNull(app, nameof(app));
Guard.NotNull(status, nameof(status));
Guard.NotNull(ids, nameof(ids));
using (Profiler.TraceMethod<MongoContentRepository>("QueryAsyncByIdsWithoutSchema"))
@ -109,7 +111,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{
Guard.NotNull(app, nameof(app));
Guard.NotNull(schema, nameof(schema));
Guard.NotNull(status, nameof(status));
using (Profiler.TraceMethod<MongoContentRepository>())
{

39
src/Squidex.Domain.Apps.Entities.MongoDb/Contents/StatusSerializer.cs

@ -0,0 +1,39 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading;
using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.Serializers;
using Squidex.Domain.Apps.Core.Contents;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{
public sealed class StatusSerializer : SerializerBase<Status2>
{
private static volatile int isRegistered;
public static void Register()
{
if (Interlocked.Increment(ref isRegistered) == 1)
{
BsonSerializer.RegisterSerializer(new StatusSerializer());
}
}
public override Status2 Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
{
var value = context.Reader.ReadString();
return new Status2(value);
}
public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, Status2 value)
{
context.Writer.WriteString(value.Name);
}
}
}

4
src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterFactory.cs

@ -162,7 +162,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors
}
filters.Add(Filter.Ne(x => x.IsDeleted, true));
if (status != null)
{
filters.Add(Filter.In(x => x.Status, status));
}
if (ids != null && ids.Count > 0)
{

58
src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs

@ -60,11 +60,13 @@ namespace Squidex.Domain.Apps.Entities.Apps
switch (command)
{
case CreateApp createApp:
return CreateAsync(createApp, c =>
return CreateReturn(createApp, c =>
{
GuardApp.CanCreate(c);
Create(c);
return Snapshot;
});
case AssignContributor assignContributor:
@ -74,111 +76,137 @@ namespace Squidex.Domain.Apps.Entities.Apps
AssignContributor(c, !Snapshot.Contributors.ContainsKey(assignContributor.ContributorId));
return EntityCreatedResult.Create(c.ContributorId, Version);
return Snapshot;
});
case RemoveContributor removeContributor:
return UpdateAsync(removeContributor, c =>
return UpdateReturn(removeContributor, c =>
{
GuardAppContributors.CanRemove(Snapshot.Contributors, c);
RemoveContributor(c);
return Snapshot;
});
case AttachClient attachClient:
return UpdateAsync(attachClient, c =>
return UpdateReturn(attachClient, c =>
{
GuardAppClients.CanAttach(Snapshot.Clients, c);
AttachClient(c);
return Snapshot;
});
case UpdateClient updateClient:
return UpdateAsync(updateClient, c =>
return UpdateReturn(updateClient, c =>
{
GuardAppClients.CanUpdate(Snapshot.Clients, c, Snapshot.Roles);
UpdateClient(c);
return Snapshot;
});
case RevokeClient revokeClient:
return UpdateAsync(revokeClient, c =>
return UpdateReturn(revokeClient, c =>
{
GuardAppClients.CanRevoke(Snapshot.Clients, c);
RevokeClient(c);
return Snapshot;
});
case AddLanguage addLanguage:
return UpdateAsync(addLanguage, c =>
return UpdateReturn(addLanguage, c =>
{
GuardAppLanguages.CanAdd(Snapshot.LanguagesConfig, c);
AddLanguage(c);
return Snapshot;
});
case RemoveLanguage removeLanguage:
return UpdateAsync(removeLanguage, c =>
return UpdateReturn(removeLanguage, c =>
{
GuardAppLanguages.CanRemove(Snapshot.LanguagesConfig, c);
RemoveLanguage(c);
return Snapshot;
});
case UpdateLanguage updateLanguage:
return UpdateAsync(updateLanguage, c =>
return UpdateReturn(updateLanguage, c =>
{
GuardAppLanguages.CanUpdate(Snapshot.LanguagesConfig, c);
UpdateLanguage(c);
return Snapshot;
});
case AddRole addRole:
return UpdateAsync(addRole, c =>
return UpdateReturn(addRole, c =>
{
GuardAppRoles.CanAdd(Snapshot.Roles, c);
AddRole(c);
return Snapshot;
});
case DeleteRole deleteRole:
return UpdateAsync(deleteRole, c =>
return UpdateReturn(deleteRole, c =>
{
GuardAppRoles.CanDelete(Snapshot.Roles, c, Snapshot.Contributors, Snapshot.Clients);
DeleteRole(c);
return Snapshot;
});
case UpdateRole updateRole:
return UpdateAsync(updateRole, c =>
return UpdateReturn(updateRole, c =>
{
GuardAppRoles.CanUpdate(Snapshot.Roles, c);
UpdateRole(c);
return Snapshot;
});
case AddPattern addPattern:
return UpdateAsync(addPattern, c =>
return UpdateReturn(addPattern, c =>
{
GuardAppPatterns.CanAdd(Snapshot.Patterns, c);
AddPattern(c);
return Snapshot;
});
case DeletePattern deletePattern:
return UpdateAsync(deletePattern, c =>
return UpdateReturn(deletePattern, c =>
{
GuardAppPatterns.CanDelete(Snapshot.Patterns, c);
DeletePattern(c);
return Snapshot;
});
case UpdatePattern updatePattern:
return UpdateAsync(updatePattern, c =>
return UpdateReturn(updatePattern, c =>
{
GuardAppPatterns.CanUpdate(Snapshot.Patterns, c);
UpdatePattern(c);
return Snapshot;
});
case ChangePlan changePlan:

14
src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByUserIndexCommandMiddleware.cs

@ -35,7 +35,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes
await Index(GetUserId(createApp)).AddAppAsync(createApp.AppId);
break;
case AssignContributor assignContributor:
await Index(GetUserId(context)).AddAppAsync(assignContributor.AppId);
await Index(GetUserId(assignContributor)).AddAppAsync(assignContributor.AppId);
break;
case RemoveContributor removeContributor:
await Index(GetUserId(removeContributor)).RemoveAppAsync(removeContributor.AppId);
@ -57,19 +57,19 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes
await next();
}
private static string GetUserId(RemoveContributor removeContributor)
private static string GetUserId(CreateApp createApp)
{
return removeContributor.ContributorId;
return createApp.Actor.Identifier;
}
private static string GetUserId(CreateApp createApp)
private static string GetUserId(AssignContributor assignContributor)
{
return createApp.Actor.Identifier;
return assignContributor.ContributorId;
}
private static string GetUserId(CommandContext context)
private static string GetUserId(RemoveContributor removeContributor)
{
return context.Result<EntityCreatedResult<string>>().IdOrValue;
return removeContributor.ContributorId;
}
private IAppsByUserIndex Index(string id)

4
src/Squidex.Domain.Apps.Entities/Apps/Invitation/InviteUserCommandMiddleware.cs

@ -35,9 +35,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.Invitation
await next();
if (assignContributor.IsCreated && context.PlainResult is EntityCreatedResult<string> id)
if (assignContributor.IsCreated && context.PlainResult is IAppEntity app)
{
context.Complete(new InvitedResult { Id = id });
context.Complete(new InvitedResult { App = app });
}
return;

4
src/Squidex.Domain.Apps.Entities/Apps/Invitation/InvitedResult.cs

@ -5,12 +5,10 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Apps.Invitation
{
public sealed class InvitedResult
{
public EntityCreatedResult<string> Id { get; set; }
public IAppEntity App { get; set; }
}
}

2
src/Squidex.Domain.Apps.Entities/Apps/RoleExtensions.cs

@ -55,7 +55,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
{
return id;
}
}));
}).Where(x => x != "common"));
}
}
}

80
src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs

@ -10,6 +10,7 @@ using System.Collections.Generic;
using System.Security.Cryptography;
using System.Threading.Tasks;
using Orleans;
using Squidex.Domain.Apps.Core.Tags;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Tags;
using Squidex.Infrastructure;
@ -24,25 +25,28 @@ namespace Squidex.Domain.Apps.Entities.Assets
private readonly IAssetQueryService assetQuery;
private readonly IAssetThumbnailGenerator assetThumbnailGenerator;
private readonly IEnumerable<ITagGenerator<CreateAsset>> tagGenerators;
private readonly ITagService tagService;
public AssetCommandMiddleware(
IGrainFactory grainFactory,
IAssetQueryService assetQuery,
IAssetStore assetStore,
IAssetThumbnailGenerator assetThumbnailGenerator,
IEnumerable<ITagGenerator<CreateAsset>> tagGenerators)
IEnumerable<ITagGenerator<CreateAsset>> tagGenerators,
ITagService tagService)
: base(grainFactory)
{
Guard.NotNull(assetStore, nameof(assetStore));
Guard.NotNull(assetQuery, nameof(assetQuery));
Guard.NotNull(assetThumbnailGenerator, nameof(assetThumbnailGenerator));
Guard.NotNull(tagGenerators, nameof(tagGenerators));
Guard.NotNull(tagService, nameof(tagService));
this.assetStore = assetStore;
this.assetQuery = assetQuery;
this.assetThumbnailGenerator = assetThumbnailGenerator;
this.tagGenerators = tagGenerators;
this.tagService = tagService;
}
public override async Task HandleAsync(CommandContext context, Func<Task> next)
@ -56,9 +60,8 @@ namespace Squidex.Domain.Apps.Entities.Assets
createAsset.Tags = new HashSet<string>();
}
createAsset.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(createAsset.File.OpenRead());
createAsset.FileHash = await UploadAsync(context, createAsset.File);
await EnrichWithImageInfosAsync(createAsset);
await EnrichWithHashAndUploadAsync(createAsset, context);
try
{
@ -70,13 +73,9 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
if (IsDuplicate(createAsset, existing))
{
result = new AssetCreatedResult(
existing.Id,
existing.Tags,
existing.Version,
existing.FileVersion,
existing.FileHash,
true);
var denormalizedTags = await tagService.DenormalizeTagsAsync(createAsset.AppId.Id, TagGroups.Assets, existing.Tags);
result = new AssetCreatedResult(existing, true, new HashSet<string>(denormalizedTags.Values));
}
break;
@ -89,17 +88,11 @@ namespace Squidex.Domain.Apps.Entities.Assets
tagGenerator.GenerateTags(createAsset, createAsset.Tags);
}
var commandResult = (AssetSavedResult)await ExecuteCommandAsync(createAsset);
var asset = (IAssetEntity)await ExecuteCommandAsync(createAsset);
result = new AssetCreatedResult(
createAsset.AssetId,
createAsset.Tags,
commandResult.Version,
commandResult.FileVersion,
commandResult.FileHash,
false);
result = new AssetCreatedResult(asset, false, createAsset.Tags);
await assetStore.CopyAsync(context.ContextId.ToString(), createAsset.AssetId.ToString(), result.FileVersion, null);
await assetStore.CopyAsync(context.ContextId.ToString(), createAsset.AssetId.ToString(), asset.FileVersion, null);
}
context.Complete(result);
@ -114,16 +107,16 @@ namespace Squidex.Domain.Apps.Entities.Assets
case UpdateAsset updateAsset:
{
updateAsset.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(updateAsset.File.OpenRead());
await EnrichWithImageInfosAsync(updateAsset);
await EnrichWithHashAndUploadAsync(updateAsset, context);
updateAsset.FileHash = await UploadAsync(context, updateAsset.File);
try
{
var result = (AssetSavedResult)await ExecuteCommandAsync(updateAsset);
var result = (AssetResult)await ExecuteAndAdjustTagsAsync(updateAsset);
context.Complete(result);
await assetStore.CopyAsync(context.ContextId.ToString(), updateAsset.AssetId.ToString(), result.FileVersion, null);
await assetStore.CopyAsync(context.ContextId.ToString(), updateAsset.AssetId.ToString(), result.Asset.FileVersion, null);
}
finally
{
@ -133,29 +126,54 @@ namespace Squidex.Domain.Apps.Entities.Assets
break;
}
case AssetCommand command:
{
var result = await ExecuteAndAdjustTagsAsync(command);
context.Complete(result);
break;
}
default:
await base.HandleAsync(context, next);
break;
}
}
private async Task<object> ExecuteAndAdjustTagsAsync(AssetCommand command)
{
var result = await ExecuteCommandAsync(command);
if (result is IAssetEntity asset)
{
var denormalizedTags = await tagService.DenormalizeTagsAsync(asset.AppId.Id, TagGroups.Assets, asset.Tags);
return new AssetResult(asset, new HashSet<string>(denormalizedTags.Values));
}
return result;
}
private static bool IsDuplicate(CreateAsset createAsset, IAssetEntity asset)
{
return asset != null && asset.FileName == createAsset.File.FileName && asset.FileSize == createAsset.File.FileSize;
}
private async Task<string> UploadAsync(CommandContext context, AssetFile file)
private async Task EnrichWithImageInfosAsync(UploadAssetCommand command)
{
string hash;
command.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(command.File.OpenRead());
}
using (var hashStream = new HasherStream(file.OpenRead(), HashAlgorithmName.SHA256))
private async Task EnrichWithHashAndUploadAsync(UploadAssetCommand command, CommandContext context)
{
using (var hashStream = new HasherStream(command.File.OpenRead(), HashAlgorithmName.SHA256))
{
await assetStore.UploadAsync(context.ContextId.ToString(), hashStream);
hash = $"{hashStream.GetHashStringAndReset()}{file.FileName}{file.FileSize}".Sha256Base64();
command.FileHash = $"{hashStream.GetHashStringAndReset()}{command.File.FileName}{command.File.FileSize}".Sha256Base64();
}
return hash;
}
}
}

19
src/Squidex.Domain.Apps.Entities/Assets/AssetCreatedResult.cs

@ -5,30 +5,17 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Assets
{
public sealed class AssetCreatedResult : EntityCreatedResult<Guid>
public sealed class AssetCreatedResult : AssetResult
{
public HashSet<string> Tags { get; }
public long FileVersion { get; }
public string FileHash { get; }
public bool IsDuplicate { get; }
public AssetCreatedResult(Guid id, HashSet<string> tags, long version, long fileVersion, string fileHash, bool isDuplicate)
: base(id, version)
public AssetCreatedResult(IAssetEntity asset, bool isDuplicate, HashSet<string> tags)
: base(asset, tags)
{
Tags = tags;
FileVersion = fileVersion;
FileHash = fileHash;
IsDuplicate = isDuplicate;
}
}

26
src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs

@ -51,16 +51,27 @@ namespace Squidex.Domain.Apps.Entities.Assets
Create(c, tagIds);
return new AssetSavedResult(Version, Snapshot.FileVersion, Snapshot.FileHash);
return Snapshot;
});
case UpdateAsset updateRule:
return UpdateAsync(updateRule, c =>
return UpdateReturn(updateRule, c =>
{
GuardAsset.CanUpdate(c);
Update(c);
return new AssetSavedResult(Version, Snapshot.FileVersion, Snapshot.FileHash);
return Snapshot;
});
case AnnotateAsset annotateAsset:
return UpdateReturnAsync(annotateAsset, async c =>
{
GuardAsset.CanAnnotate(c, Snapshot.FileName, Snapshot.Slug);
var tagIds = await NormalizeTagsAsync(Snapshot.AppId.Id, c.Tags);
Annotate(c, tagIds);
return Snapshot;
});
case DeleteAsset deleteAsset:
return UpdateAsync(deleteAsset, async c =>
@ -71,15 +82,6 @@ namespace Squidex.Domain.Apps.Entities.Assets
Delete(c);
});
case AnnotateAsset annotateAsset:
return UpdateAsync(annotateAsset, async c =>
{
GuardAsset.CanAnnotate(c, Snapshot.FileName, Snapshot.Slug);
var tagIds = await NormalizeTagsAsync(Snapshot.AppId.Id, c.Tags);
Annotate(c, tagIds);
});
default:
throw new NotSupportedException();
}

25
src/Squidex.Domain.Apps.Entities/Assets/AssetResult.cs

@ -0,0 +1,25 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
namespace Squidex.Domain.Apps.Entities.Assets
{
public class AssetResult
{
public IAssetEntity Asset { get; }
public HashSet<string> Tags { get; }
public AssetResult(IAssetEntity asset, HashSet<string> tags)
{
Asset = asset;
Tags = tags;
}
}
}

25
src/Squidex.Domain.Apps.Entities/Assets/AssetSavedResult.cs

@ -1,25 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Assets
{
public class AssetSavedResult : EntitySavedResult
{
public long FileVersion { get; }
public string FileHash { get; }
public AssetSavedResult(long version, long fileVersion, string fileHash)
: base(version)
{
FileVersion = fileVersion;
FileHash = fileHash;
}
}
}

9
src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs

@ -8,22 +8,15 @@
using System;
using System.Collections.Generic;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets;
namespace Squidex.Domain.Apps.Entities.Assets.Commands
{
public sealed class CreateAsset : AssetCommand, IAppCommand
public sealed class CreateAsset : UploadAssetCommand, IAppCommand
{
public NamedId<Guid> AppId { get; set; }
public AssetFile File { get; set; }
public ImageInfo ImageInfo { get; set; }
public HashSet<string> Tags { get; set; }
public string FileHash { get; set; }
public CreateAsset()
{
AssetId = Guid.NewGuid();

9
src/Squidex.Domain.Apps.Entities/Assets/Commands/UpdateAsset.cs

@ -5,16 +5,9 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure.Assets;
namespace Squidex.Domain.Apps.Entities.Assets.Commands
{
public sealed class UpdateAsset : AssetCommand
public sealed class UpdateAsset : UploadAssetCommand
{
public AssetFile File { get; set; }
public ImageInfo ImageInfo { get; set; }
public string FileHash { get; set; }
}
}

20
src/Squidex.Domain.Apps.Entities/Assets/Commands/UploadAssetCommand.cs

@ -0,0 +1,20 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure.Assets;
namespace Squidex.Domain.Apps.Entities.Assets.Commands
{
public abstract class UploadAssetCommand : AssetCommand
{
public AssetFile File { get; set; }
public ImageInfo ImageInfo { get; set; }
public string FileHash { get; set; }
}
}

6
src/Squidex.Domain.Apps.Entities/Comments/CommentsGrain.cs

@ -73,7 +73,7 @@ namespace Squidex.Domain.Apps.Entities.Comments
switch (command)
{
case CreateComment createComment:
return UpsertAsync(createComment, c =>
return UpsertReturn(createComment, c =>
{
GuardComments.CanCreate(c);
@ -83,7 +83,7 @@ namespace Squidex.Domain.Apps.Entities.Comments
});
case UpdateComment updateComment:
return UpsertAsync(updateComment, c =>
return Upsert(updateComment, c =>
{
GuardComments.CanUpdate(events, c);
@ -91,7 +91,7 @@ namespace Squidex.Domain.Apps.Entities.Comments
});
case DeleteComment deleteComment:
return UpsertAsync(deleteComment, c =>
return Upsert(deleteComment, c =>
{
GuardComments.CanDelete(events, c);

23
src/Squidex.Domain.Apps.Entities/Contents/ContentDataChangedResult.cs

@ -1,23 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Contents
{
public sealed class ContentDataChangedResult : EntitySavedResult
{
public NamedContentData Data { get; }
public ContentDataChangedResult(NamedContentData data, long version)
: base(version)
{
Data = data;
}
}
}

26
src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs

@ -81,7 +81,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
Create(c);
return EntityCreatedResult.Create(c.Data, Version);
return Snapshot;
});
case UpdateContent updateContent:
@ -101,7 +101,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
});
case ChangeContentStatus changeContentStatus:
return UpdateAsync(changeContentStatus, async c =>
return UpdateReturnAsync(changeContentStatus, async c =>
{
try
{
@ -157,6 +157,18 @@ namespace Squidex.Domain.Apps.Entities.Contents
throw;
}
}
return Snapshot;
});
case DiscardChanges discardChanges:
return UpdateReturn(discardChanges, c =>
{
GuardContent.CanDiscardChanges(Snapshot.IsPending, c);
DiscardChanges(c);
return Snapshot;
});
case DeleteContent deleteContent:
@ -171,14 +183,6 @@ namespace Squidex.Domain.Apps.Entities.Contents
Delete(c);
});
case DiscardChanges discardChanges:
return UpdateAsync(discardChanges, c =>
{
GuardContent.CanDiscardChanges(Snapshot.IsPending, c);
DiscardChanges(c);
});
default:
throw new NotSupportedException();
}
@ -220,7 +224,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
}
}
return new ContentDataChangedResult(newData, Version);
return Snapshot;
}
public void Create(CreateContent command)

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

@ -33,10 +33,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
public sealed class ContentQueryService : IContentQueryService
{
private static readonly Status[] StatusAll = { Status.Archived, Status.Draft, Status.Published };
private static readonly Status[] StatusArchived = { Status.Archived };
private static readonly Status[] StatusPublishedOnly = { Status.Published };
private static readonly Status[] StatusPublishedDraft = { Status.Published, Status.Draft };
private readonly IContentRepository contentRepository;
private readonly IContentVersionLoader contentVersionLoader;
private readonly IAppProvider appProvider;
@ -93,7 +90,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
var isVersioned = version > EtagVersion.Empty;
var status = GetFindStatus(context);
var status = GetStatus(context);
var content =
isVersioned ?
@ -119,7 +116,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
using (Profiler.TraceMethod<ContentQueryService>())
{
var status = GetQueryStatus(context);
var status = GetStatus(context);
IResultList<IContentEntity> contents;
@ -145,7 +142,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
using (Profiler.TraceMethod<ContentQueryService>())
{
var status = GetQueryStatus(context);
var status = GetStatus(context);
List<IContentEntity> result;
@ -217,7 +214,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
result.Data = result.Data.ConvertName2Name(schema.SchemaDef, converters);
}
if (result.DataDraft != null && (context.ApiStatus == StatusForApi.PublishedDraft || context.IsFrontendClient))
if (result.DataDraft != null && (context.ApiStatus == StatusForApi.All || context.IsFrontendClient))
{
result.DataDraft = result.DataDraft.ConvertName2Name(schema.SchemaDef, converters);
}
@ -340,47 +337,17 @@ namespace Squidex.Domain.Apps.Entities.Contents
return permissions.Allows(permission);
}
private static Status[] GetFindStatus(QueryContext context)
private static Status[] GetStatus(QueryContext context)
{
if (context.IsFrontendClient)
if (context.IsFrontendClient || context.ApiStatus == StatusForApi.All)
{
return StatusAll;
}
else if (context.ApiStatus == StatusForApi.PublishedDraft)
{
return StatusPublishedDraft;
}
else
{
return StatusPublishedOnly;
}
}
private static Status[] GetQueryStatus(QueryContext context)
{
if (context.IsFrontendClient)
{
switch (context.FrontendStatus)
{
case StatusForFrontend.Archived:
return StatusArchived;
case StatusForFrontend.PublishedOnly:
return StatusPublishedOnly;
default:
return StatusPublishedDraft;
}
return null;
}
else
{
switch (context.ApiStatus)
{
case StatusForApi.PublishedDraft:
return StatusPublishedDraft;
default:
return StatusPublishedOnly;
}
}
}
private Task<List<(IContentEntity Content, ISchemaEntity Schema)>> QueryAsync(QueryContext context, IReadOnlyList<Guid> ids, Status[] status)
{
@ -409,7 +376,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
private static bool ShouldIncludeDraft(QueryContext context)
{
return context.ApiStatus == StatusForApi.PublishedDraft || context.IsFrontendClient;
return context.ApiStatus == StatusForApi.All || context.IsFrontendClient;
}
}
}

24
src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs

@ -0,0 +1,24 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Schemas;
namespace Squidex.Domain.Apps.Entities.Contents
{
public interface IContentWorkflow
{
Task<Status2> GetInitialStatusAsync(ISchemaEntity schema);
Task<bool> IsValidNextStatus(IContentEntity content, Status2 next);
Task<Status2[]> GetNextsAsync(IContentEntity content);
Task<Status2[]> GetAllAsync(ISchemaEntity schema);
}
}

2
src/Squidex.Domain.Apps.Entities/Contents/StatusForApi.cs

@ -10,6 +10,6 @@ namespace Squidex.Domain.Apps.Entities.Contents
public enum StatusForApi
{
PublishedOnly,
PublishedDraft,
All,
}
}

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

@ -27,8 +27,6 @@ namespace Squidex.Domain.Apps.Entities
public StatusForApi ApiStatus { get; private set; }
public StatusForFrontend FrontendStatus { get; private set; }
public IReadOnlyCollection<string> AssetUrlsToResolve { get; private set; }
public IReadOnlyCollection<Language> Languages { get; private set; }
@ -49,7 +47,7 @@ namespace Squidex.Domain.Apps.Entities
public QueryContext WithUnpublished(bool unpublished)
{
return WithApiStatus(unpublished ? StatusForApi.PublishedDraft : StatusForApi.PublishedOnly);
return WithApiStatus(unpublished ? StatusForApi.All : StatusForApi.PublishedOnly);
}
public QueryContext WithApiStatus(StatusForApi status)
@ -57,21 +55,6 @@ namespace Squidex.Domain.Apps.Entities
return Clone(c => c.ApiStatus = status);
}
public QueryContext WithFrontendStatus(StatusForFrontend status)
{
return Clone(c => c.FrontendStatus = status);
}
public QueryContext WithFrontendStatus(string status)
{
if (status != null && Enum.TryParse<StatusForFrontend>(status, out var result))
{
return WithFrontendStatus(result);
}
return this;
}
public QueryContext WithAssetUrlsToResolve(IEnumerable<string> fieldNames)
{
if (fieldNames != null)

18
src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs

@ -41,35 +41,43 @@ namespace Squidex.Domain.Apps.Entities.Rules
switch (command)
{
case CreateRule createRule:
return CreateAsync(createRule, async c =>
return CreateReturnAsync(createRule, async c =>
{
await GuardRule.CanCreate(c, appProvider);
Create(c);
return Snapshot;
});
case UpdateRule updateRule:
return UpdateAsync(updateRule, async c =>
return UpdateReturnAsync(updateRule, async c =>
{
await GuardRule.CanUpdate(c, Snapshot.AppId.Id, appProvider);
Update(c);
return Snapshot;
});
case EnableRule enableRule:
return UpdateAsync(enableRule, c =>
return UpdateReturn(enableRule, c =>
{
GuardRule.CanEnable(c, Snapshot.RuleDef);
Enable(c);
return Snapshot;
});
case DisableRule disableRule:
return UpdateAsync(disableRule, c =>
return UpdateReturn(disableRule, c =>
{
GuardRule.CanDisable(c, Snapshot.RuleDef);
Disable(c);
return Snapshot;
});
case DeleteRule deleteRule:
return UpdateAsync(deleteRule, c =>
return Update(deleteRule, c =>
{
GuardRule.CanDelete(deleteRule);

24
src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardHelper.cs

@ -12,21 +12,26 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
{
public static class GuardHelper
{
public static IArrayField GetArrayFieldOrThrow(Schema schema, long parentId)
public static IArrayField GetArrayFieldOrThrow(Schema schema, long parentId, bool allowLocked)
{
if (!schema.FieldsById.TryGetValue(parentId, out var rootField) || !(rootField is IArrayField arrayField))
{
throw new DomainObjectNotFoundException(parentId.ToString(), "Fields", typeof(Schema));
}
if (!allowLocked)
{
EnsureNotLocked(arrayField);
}
return arrayField;
}
public static IField GetFieldOrThrow(Schema schema, long fieldId, long? parentId)
public static IField GetFieldOrThrow(Schema schema, long fieldId, long? parentId, bool allowLocked)
{
if (parentId.HasValue)
{
var arrayField = GetArrayFieldOrThrow(schema, parentId.Value);
var arrayField = GetArrayFieldOrThrow(schema, parentId.Value, allowLocked);
if (!arrayField.FieldsById.TryGetValue(fieldId, out var nestedField))
{
@ -41,7 +46,20 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
throw new DomainObjectNotFoundException(fieldId.ToString(), "Fields", typeof(Schema));
}
if (!allowLocked)
{
EnsureNotLocked(field);
}
return field;
}
private static void EnsureNotLocked(IField field)
{
if (field.IsLocked)
{
throw new DomainException("Schema field is locked.");
}
}
}
}

2
src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs

@ -56,7 +56,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
if (command.ParentFieldId.HasValue)
{
arrayField = GuardHelper.GetArrayFieldOrThrow(schema, command.ParentFieldId.Value);
arrayField = GuardHelper.GetArrayFieldOrThrow(schema, command.ParentFieldId.Value, false);
}
Validate.It(() => "Cannot reorder schema fields.", error =>

50
src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchemaField.cs

@ -38,7 +38,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
if (command.ParentFieldId.HasValue)
{
var arrayField = GuardHelper.GetArrayFieldOrThrow(schema, command.ParentFieldId.Value);
var arrayField = GuardHelper.GetArrayFieldOrThrow(schema, command.ParentFieldId.Value, false);
if (arrayField.FieldsByName.ContainsKey(command.Name))
{
@ -64,12 +64,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
{
Guard.NotNull(command, nameof(command));
var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId);
if (field.IsLocked)
{
throw new DomainException("Schema field is already locked.");
}
var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false);
Validate.It(() => "Cannot update field.", e =>
{
@ -90,12 +85,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
{
Guard.NotNull(command, nameof(command));
var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId);
if (field.IsLocked)
{
throw new DomainException("Schema field is locked.");
}
var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false);
if (field.IsHidden)
{
@ -108,18 +98,30 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
}
}
public static void CanShow(Schema schema, ShowField command)
{
Guard.NotNull(command, nameof(command));
var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false);
if (!field.IsHidden)
{
throw new DomainException("Schema field is already visible.");
}
}
public static void CanDisable(Schema schema, DisableField command)
{
Guard.NotNull(command, nameof(command));
var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId);
var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false);
if (field.IsDisabled)
{
throw new DomainException("Schema field is already disabled.");
}
if (!field.IsForApi())
if (!field.IsForApi(true))
{
throw new DomainException("UI field cannot be disabled.");
}
@ -129,7 +131,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
{
Guard.NotNull(command, nameof(command));
var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId);
var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false);
if (field.IsLocked)
{
@ -137,23 +139,11 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
}
}
public static void CanShow(Schema schema, ShowField command)
{
Guard.NotNull(command, nameof(command));
var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId);
if (!field.IsHidden)
{
throw new DomainException("Schema field is already visible.");
}
}
public static void CanEnable(Schema schema, EnableField command)
{
Guard.NotNull(command, nameof(command));
var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId);
var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false);
if (!field.IsDisabled)
{
@ -165,7 +155,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
{
Guard.NotNull(command, nameof(command));
var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId);
var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false);
if (field.IsLocked)
{

70
src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs

@ -48,7 +48,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
switch (command)
{
case AddField addField:
return UpdateAsync(addField, c =>
return UpdateReturn(addField, c =>
{
GuardSchemaField.CanAdd(Snapshot.SchemaDef, c);
@ -65,139 +65,171 @@ namespace Squidex.Domain.Apps.Entities.Schemas
id = ((IArrayField)Snapshot.SchemaDef.FieldsById[c.ParentFieldId.Value]).FieldsByName[c.Name].Id;
}
return EntityCreatedResult.Create(id, Version);
return Snapshot;
});
case CreateSchema createSchema:
return CreateAsync(createSchema, async c =>
return CreateReturnAsync(createSchema, async c =>
{
await GuardSchema.CanCreate(c, appProvider);
Create(c);
return Snapshot;
});
case SynchronizeSchema synchronizeSchema:
return UpdateAsync(synchronizeSchema, c =>
return UpdateReturn(synchronizeSchema, c =>
{
GuardSchema.CanSynchronize(c);
Synchronize(c);
return Snapshot;
});
case DeleteField deleteField:
return UpdateAsync(deleteField, c =>
return UpdateReturn(deleteField, c =>
{
GuardSchemaField.CanDelete(Snapshot.SchemaDef, deleteField);
DeleteField(c);
return Snapshot;
});
case LockField lockField:
return UpdateAsync(lockField, c =>
return UpdateReturn(lockField, c =>
{
GuardSchemaField.CanLock(Snapshot.SchemaDef, lockField);
LockField(c);
return Snapshot;
});
case HideField hideField:
return UpdateAsync(hideField, c =>
return UpdateReturn(hideField, c =>
{
GuardSchemaField.CanHide(Snapshot.SchemaDef, c);
HideField(c);
return Snapshot;
});
case ShowField showField:
return UpdateAsync(showField, c =>
return UpdateReturn(showField, c =>
{
GuardSchemaField.CanShow(Snapshot.SchemaDef, c);
ShowField(c);
return Snapshot;
});
case DisableField disableField:
return UpdateAsync(disableField, c =>
return UpdateReturn(disableField, c =>
{
GuardSchemaField.CanDisable(Snapshot.SchemaDef, c);
DisableField(c);
return Snapshot;
});
case EnableField enableField:
return UpdateAsync(enableField, c =>
return UpdateReturn(enableField, c =>
{
GuardSchemaField.CanEnable(Snapshot.SchemaDef, c);
EnableField(c);
return Snapshot;
});
case UpdateField updateField:
return UpdateAsync(updateField, c =>
return UpdateReturn(updateField, c =>
{
GuardSchemaField.CanUpdate(Snapshot.SchemaDef, c);
UpdateField(c);
return Snapshot;
});
case ReorderFields reorderFields:
return UpdateAsync(reorderFields, c =>
return UpdateReturn(reorderFields, c =>
{
GuardSchema.CanReorder(Snapshot.SchemaDef, c);
Reorder(c);
return Snapshot;
});
case UpdateSchema updateSchema:
return UpdateAsync(updateSchema, c =>
return UpdateReturn(updateSchema, c =>
{
GuardSchema.CanUpdate(Snapshot.SchemaDef, c);
Update(c);
return Snapshot;
});
case PublishSchema publishSchema:
return UpdateAsync(publishSchema, c =>
return UpdateReturn(publishSchema, c =>
{
GuardSchema.CanPublish(Snapshot.SchemaDef, c);
Publish(c);
return Snapshot;
});
case UnpublishSchema unpublishSchema:
return UpdateAsync(unpublishSchema, c =>
return UpdateReturn(unpublishSchema, c =>
{
GuardSchema.CanUnpublish(Snapshot.SchemaDef, c);
Unpublish(c);
return Snapshot;
});
case ConfigureScripts configureScripts:
return UpdateAsync(configureScripts, c =>
return UpdateReturn(configureScripts, c =>
{
GuardSchema.CanConfigureScripts(Snapshot.SchemaDef, c);
ConfigureScripts(c);
return Snapshot;
});
case ChangeCategory changeCategory:
return UpdateAsync(changeCategory, c =>
return UpdateReturn(changeCategory, c =>
{
GuardSchema.CanChangeCategory(Snapshot.SchemaDef, c);
ChangeCategory(c);
return Snapshot;
});
case ConfigurePreviewUrls configurePreviewUrls:
return UpdateAsync(configurePreviewUrls, c =>
return UpdateReturn(configurePreviewUrls, c =>
{
GuardSchema.CanConfigurePreviewUrls(c);
ConfigurePreviewUrls(c);
return Snapshot;
});
case DeleteSchema deleteSchema:
return UpdateAsync(deleteSchema, c =>
return Update(deleteSchema, c =>
{
GuardSchema.CanDelete(Snapshot.SchemaDef, c);

16
src/Squidex.Domain.Users/UserManagerExtensions.cs

@ -115,7 +115,7 @@ namespace Squidex.Domain.Users
return result;
}
public static async Task<IdentityUser> CreateAsync(this UserManager<IdentityUser> userManager, IUserFactory factory, UserValues values)
public static async Task<UserWithClaims> CreateAsync(this UserManager<IdentityUser> userManager, IUserFactory factory, UserValues values)
{
var user = factory.Create(values.Email);
@ -142,10 +142,10 @@ namespace Squidex.Domain.Users
throw;
}
return user;
return await userManager.ResolveUserAsync(user);
}
public static async Task UpdateAsync(this UserManager<IdentityUser> userManager, string id, UserValues values)
public static async Task<UserWithClaims> UpdateAsync(this UserManager<IdentityUser> userManager, string id, UserValues values)
{
var user = await userManager.FindByIdAsync(id);
@ -155,6 +155,8 @@ namespace Squidex.Domain.Users
}
await UpdateAsync(userManager, user, values);
return await userManager.ResolveUserAsync(user);
}
public static async Task<IdentityResult> UpdateSafeAsync(this UserManager<IdentityUser> userManager, IdentityUser user, UserValues values)
@ -193,7 +195,7 @@ namespace Squidex.Domain.Users
}
}
public static async Task LockAsync(this UserManager<IdentityUser> userManager, string id)
public static async Task<UserWithClaims> LockAsync(this UserManager<IdentityUser> userManager, string id)
{
var user = await userManager.FindByIdAsync(id);
@ -203,9 +205,11 @@ namespace Squidex.Domain.Users
}
await DoChecked(() => userManager.SetLockoutEndDateAsync(user, DateTimeOffset.UtcNow.AddYears(100)), "Cannot lock user.");
return await userManager.ResolveUserAsync(user);
}
public static async Task UnlockAsync(this UserManager<IdentityUser> userManager, string id)
public static async Task<UserWithClaims> UnlockAsync(this UserManager<IdentityUser> userManager, string id)
{
var user = await userManager.FindByIdAsync(id);
@ -215,6 +219,8 @@ namespace Squidex.Domain.Users
}
await DoChecked(() => userManager.SetLockoutEndDateAsync(user, null), "Cannot unlock user.");
return await userManager.ResolveUserAsync(user);
}
private static async Task DoChecked(Func<Task<IdentityResult>> action, string message)

8
src/Squidex.Infrastructure/CollectionExtensions.cs

@ -13,6 +13,14 @@ namespace Squidex.Infrastructure
{
public static class CollectionExtensions
{
public static void AddRange<T>(this ICollection<T> target, IEnumerable<T> source)
{
foreach (var value in source)
{
target.Add(value);
}
}
public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> enumerable)
{
var random = new Random();

12
src/Squidex.Infrastructure/Commands/DomainObjectGrainBase.cs

@ -93,7 +93,7 @@ namespace Squidex.Infrastructure.Commands
return InvokeAsync(command, handler, Mode.Create);
}
protected Task<object> CreateReturnAsync<TCommand>(TCommand command, Func<TCommand, object> handler) where TCommand : class, IAggregateCommand
protected Task<object> CreateReturn<TCommand>(TCommand command, Func<TCommand, object> handler) where TCommand : class, IAggregateCommand
{
return InvokeAsync(command, handler?.ToAsync(), Mode.Create);
}
@ -103,7 +103,7 @@ namespace Squidex.Infrastructure.Commands
return InvokeAsync(command, handler.ToDefault<TCommand, object>(), Mode.Create);
}
protected Task<object> CreateAsync<TCommand>(TCommand command, Action<TCommand> handler) where TCommand : class, IAggregateCommand
protected Task<object> Create<TCommand>(TCommand command, Action<TCommand> handler) where TCommand : class, IAggregateCommand
{
return InvokeAsync(command, handler?.ToDefault<TCommand, object>()?.ToAsync(), Mode.Create);
}
@ -113,7 +113,7 @@ namespace Squidex.Infrastructure.Commands
return InvokeAsync(command, handler, Mode.Update);
}
protected Task<object> UpdateAsync<TCommand>(TCommand command, Func<TCommand, object> handler) where TCommand : class, IAggregateCommand
protected Task<object> UpdateReturn<TCommand>(TCommand command, Func<TCommand, object> handler) where TCommand : class, IAggregateCommand
{
return InvokeAsync(command, handler?.ToAsync(), Mode.Update);
}
@ -123,7 +123,7 @@ namespace Squidex.Infrastructure.Commands
return InvokeAsync(command, handler?.ToDefault<TCommand, object>(), Mode.Update);
}
protected Task<object> UpdateAsync<TCommand>(TCommand command, Action<TCommand> handler) where TCommand : class, IAggregateCommand
protected Task<object> Update<TCommand>(TCommand command, Action<TCommand> handler) where TCommand : class, IAggregateCommand
{
return InvokeAsync(command, handler?.ToDefault<TCommand, object>()?.ToAsync(), Mode.Update);
}
@ -133,7 +133,7 @@ namespace Squidex.Infrastructure.Commands
return InvokeAsync(command, handler, Mode.Upsert);
}
protected Task<object> UpsertAsync<TCommand>(TCommand command, Func<TCommand, object> handler) where TCommand : class, IAggregateCommand
protected Task<object> UpsertReturn<TCommand>(TCommand command, Func<TCommand, object> handler) where TCommand : class, IAggregateCommand
{
return InvokeAsync(command, handler?.ToAsync(), Mode.Upsert);
}
@ -143,7 +143,7 @@ namespace Squidex.Infrastructure.Commands
return InvokeAsync(command, handler?.ToDefault<TCommand, object>(), Mode.Upsert);
}
protected Task<object> UpsertAsync<TCommand>(TCommand command, Action<TCommand> handler) where TCommand : class, IAggregateCommand
protected Task<object> Upsert<TCommand>(TCommand command, Action<TCommand> handler) where TCommand : class, IAggregateCommand
{
return InvokeAsync(command, handler?.ToDefault<TCommand, object>()?.ToAsync(), Mode.Upsert);
}

29
src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs

@ -58,7 +58,12 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
public Task<Immutable<EventConsumerInfo>> GetStateAsync()
{
return Task.FromResult(State.ToInfo(eventConsumer.Name).AsImmutable());
return Task.FromResult(CreateInfo());
}
private Immutable<EventConsumerInfo> CreateInfo()
{
return State.ToInfo(eventConsumer.Name).AsImmutable();
}
public Task OnEventAsync(Immutable<IEventSubscription> subscription, Immutable<StoredEvent> storedEvent)
@ -109,39 +114,43 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
return TaskHelper.Done;
}
public Task StartAsync()
public async Task<Immutable<EventConsumerInfo>> StartAsync()
{
if (!State.IsStopped)
{
return TaskHelper.Done;
return CreateInfo();
}
return DoAndUpdateStateAsync(() =>
await DoAndUpdateStateAsync(() =>
{
Subscribe(State.Position);
State = State.Started();
});
return CreateInfo();
}
public Task StopAsync()
public async Task<Immutable<EventConsumerInfo>> StopAsync()
{
if (State.IsStopped)
{
return TaskHelper.Done;
return CreateInfo();
}
return DoAndUpdateStateAsync(() =>
await DoAndUpdateStateAsync(() =>
{
Unsubscribe();
State = State.Stopped();
});
return CreateInfo();
}
public Task ResetAsync()
public async Task<Immutable<EventConsumerInfo>> ResetAsync()
{
return DoAndUpdateStateAsync(async () =>
await DoAndUpdateStateAsync(async () =>
{
Unsubscribe();
@ -151,6 +160,8 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
State = State.Reset();
});
return CreateInfo();
}
private Task DoAndUpdateStateAsync(Action action, [CallerMemberName] string caller = null)

12
src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerManagerGrain.cs

@ -74,33 +74,31 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
{
return Task.WhenAll(
eventConsumers
.Select(c => GrainFactory.GetGrain<IEventConsumerGrain>(c.Name))
.Select(c => c.StartAsync()));
.Select(c => StartAsync(c.Name)));
}
public Task StopAllAsync()
{
return Task.WhenAll(
eventConsumers
.Select(c => GrainFactory.GetGrain<IEventConsumerGrain>(c.Name))
.Select(c => c.StopAsync()));
.Select(c => StopAsync(c.Name)));
}
public Task ResetAsync(string consumerName)
public Task<Immutable<EventConsumerInfo>> ResetAsync(string consumerName)
{
var eventConsumer = GrainFactory.GetGrain<IEventConsumerGrain>(consumerName);
return eventConsumer.ResetAsync();
}
public Task StartAsync(string consumerName)
public Task<Immutable<EventConsumerInfo>> StartAsync(string consumerName)
{
var eventConsumer = GrainFactory.GetGrain<IEventConsumerGrain>(consumerName);
return eventConsumer.StartAsync();
}
public Task StopAsync(string consumerName)
public Task<Immutable<EventConsumerInfo>> StopAsync(string consumerName)
{
var eventConsumer = GrainFactory.GetGrain<IEventConsumerGrain>(consumerName);

6
src/Squidex.Infrastructure/EventSourcing/Grains/IEventConsumerGrain.cs

@ -16,11 +16,11 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
{
Task<Immutable<EventConsumerInfo>> GetStateAsync();
Task StopAsync();
Task<Immutable<EventConsumerInfo>> StopAsync();
Task StartAsync();
Task<Immutable<EventConsumerInfo>> StartAsync();
Task ResetAsync();
Task<Immutable<EventConsumerInfo>> ResetAsync();
Task OnEventAsync(Immutable<IEventSubscription> subscription, Immutable<StoredEvent> storedEvent);

10
src/Squidex.Infrastructure/EventSourcing/Grains/IEventConsumerManagerGrain.cs

@ -16,15 +16,15 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
{
Task ActivateAsync(string streamName);
Task StopAllAsync();
Task StartAllAsync();
Task StopAsync(string consumerName);
Task StopAllAsync();
Task StartAllAsync();
Task<Immutable<EventConsumerInfo>> StopAsync(string consumerName);
Task StartAsync(string consumerName);
Task<Immutable<EventConsumerInfo>> StartAsync(string consumerName);
Task ResetAsync(string consumerName);
Task<Immutable<EventConsumerInfo>> ResetAsync(string consumerName);
Task<Immutable<List<EventConsumerInfo>>> GetConsumersAsync();
}

10
src/Squidex.Shared/Permissions.cs

@ -37,8 +37,6 @@ namespace Squidex.Shared
public const string AdminAppCreate = "squidex.admin.apps.create";
public const string AdminRestore = "squidex.admin.restore";
public const string AdminRestoreRead = "squidex.admin.restore.read";
public const string AdminRestoreCreate = "squidex.admin.restore.create";
public const string AdminEvents = "squidex.admin.events";
public const string AdminEventsRead = "squidex.admin.events.read";
@ -79,7 +77,6 @@ namespace Squidex.Shared
public const string AppRolesDelete = "squidex.apps.{app}.roles.delete";
public const string AppPatterns = "squidex.apps.{app}.patterns";
public const string AppPatternsRead = "squidex.apps.{app}.patterns.read";
public const string AppPatternsCreate = "squidex.apps.{app}.patterns.create";
public const string AppPatternsUpdate = "squidex.apps.{app}.patterns.update";
public const string AppPatternsDelete = "squidex.apps.{app}.patterns.delete";
@ -108,7 +105,6 @@ namespace Squidex.Shared
public const string AppRulesDelete = "squidex.apps.{app}.rules.delete";
public const string AppSchemas = "squidex.apps.{app}.schemas.{name}";
public const string AppSchemasRead = "squidex.apps.{app}.schemas.{name}.read";
public const string AppSchemasCreate = "squidex.apps.{app}.schemas.{name}.create";
public const string AppSchemasUpdate = "squidex.apps.{app}.schemas.{name}.update";
public const string AppSchemasScripts = "squidex.apps.{app}.schemas.{name}.scripts";
@ -117,14 +113,10 @@ namespace Squidex.Shared
public const string AppContents = "squidex.apps.{app}.contents.{name}";
public const string AppContentsRead = "squidex.apps.{app}.contents.{name}.read";
public const string AppContentsGraphQL = "squidex.apps.{app}.contents.{name}.graphql";
public const string AppContentsCreate = "squidex.apps.{app}.contents.{name}.create";
public const string AppContentsUpdate = "squidex.apps.{app}.contents.{name}.update";
public const string AppContentsStatus = "squidex.apps.{app}.contents.{name}.status.{status}";
public const string AppContentsDiscard = "squidex.apps.{app}.contents.{name}.discard";
public const string AppContentsArchive = "squidex.apps.{app}.contents.{name}.archive";
public const string AppContentsRestore = "squidex.apps.{app}.contents.{name}.restore";
public const string AppContentsPublish = "squidex.apps.{app}.contents.{name}.publish";
public const string AppContentsUnpublish = "squidex.apps.{app}.contents.{name}.unpublish";
public const string AppContentsDelete = "squidex.apps.{app}.contents.{name}.delete";
public const string AppApi = "squidex.apps.{app}.api";

8
src/Squidex.Web/Extensions.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Security.Claims;
using Squidex.Infrastructure.Security;
@ -40,5 +41,12 @@ namespace Squidex.Web
return (null, null);
}
public static bool IsUser(this ApiController controller, string userId)
{
var subject = controller.User.OpenIdSubject();
return string.Equals(subject, userId, StringComparison.OrdinalIgnoreCase);
}
}
}

76
src/Squidex.Web/PermissionExtensions.cs

@ -0,0 +1,76 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
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;
}
public static bool HasPermission(this HttpContext httpContext, Permission permission, PermissionSet permissions = null)
{
return httpContext.Permissions().Includes(permission) || permissions?.Includes(permission) == true;
}
public static bool HasPermission(this HttpContext httpContext, string id, string app = "*", string schema = "*", PermissionSet permissions = null)
{
return httpContext.HasPermission(Shared.Permissions.ForApp(id, app, schema), permissions);
}
public static bool HasPermission(this ApiController controller, Permission permission, PermissionSet permissions = null)
{
return controller.HttpContext.HasPermission(permission, permissions);
}
public static bool HasPermission(this ApiController controller, string id, string app = "*", string schema = "*", PermissionSet permissions = null)
{
if (app == "*")
{
if (controller.RouteData.Values.TryGetValue("app", out var value) && value is string s)
{
app = s;
}
}
if (schema == "*")
{
if (controller.RouteData.Values.TryGetValue("name", out var value) && value is string s)
{
schema = s;
}
}
return controller.HasPermission(Shared.Permissions.ForApp(id, app, schema), permissions);
}
}
}

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

@ -65,7 +65,10 @@ namespace Squidex.Web.Pipeline
{
var identity = user.Identities.First();
if (!string.IsNullOrWhiteSpace(role))
{
identity.AddClaim(new Claim(ClaimTypes.Role, role));
}
foreach (var permission in permissions)
{

61
src/Squidex.Web/Resource.cs

@ -0,0 +1,61 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Newtonsoft.Json;
using Squidex.Infrastructure;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace Squidex.Web
{
public abstract class Resource
{
[JsonProperty("_links")]
[Required]
[Display(Description = "The links.")]
public Dictionary<string, ResourceLink> Links { get; } = new Dictionary<string, ResourceLink>();
public void AddSelfLink(string href)
{
AddGetLink("self", href);
}
public void AddGetLink(string rel, string href)
{
AddLink(rel, "GET", href);
}
public void AddPatchLink(string rel, string href)
{
AddLink(rel, "PATCH", href);
}
public void AddPostLink(string rel, string href)
{
AddLink(rel, "POST", href);
}
public void AddPutLink(string rel, string href)
{
AddLink(rel, "PUT", href);
}
public void AddDeleteLink(string rel, string href)
{
AddLink(rel, "DELETE", href);
}
public void AddLink(string rel, string method, string href)
{
Guard.NotNullOrEmpty(rel, nameof(rel));
Guard.NotNullOrEmpty(href, nameof(href));
Guard.NotNullOrEmpty(method, nameof(method));
Links[rel] = new ResourceLink { Href = href, Method = method };
}
}
}

16
src/Squidex/Areas/Api/Controllers/Users/Models/UserCreatedDto.cs → src/Squidex.Web/ResourceLink.cs

@ -7,20 +7,16 @@
using System.ComponentModel.DataAnnotations;
namespace Squidex.Areas.Api.Controllers.Users.Models
namespace Squidex.Web
{
public sealed class UserCreatedDto
public class ResourceLink
{
/// <summary>
/// The id of the user.
/// </summary>
[Required]
public string Id { get; set; }
[Display(Description = "The link url.")]
public string Href { get; set; }
/// <summary>
/// Additional permissions for the user.
/// </summary>
[Required]
public string[] Permissions { get; set; }
[Display(Description = "The link method.")]
public string Method { get; set; }
}
}

44
src/Squidex.Web/UrlHelperExtensions.cs

@ -0,0 +1,44 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.AspNetCore.Mvc;
using System;
namespace Squidex.Web
{
public static class UrlHelperExtensions
{
private static class NameOf<T>
{
public static readonly string Controller;
static NameOf()
{
const string suffix = "Controller";
var name = typeof(T).Name;
if (name.EndsWith(suffix))
{
name = name.Substring(0, name.Length - suffix.Length);
}
Controller = name;
}
}
public static string Url<T>(this IUrlHelper urlHelper, Func<T, string> action, object values = null) where T : Controller
{
return urlHelper.Action(action(null), NameOf<T>.Controller, values);
}
public static string Url<T>(this Controller controller, Func<T, string> action, object values = null) where T : Controller
{
return controller.Url.Url<T>(action, values);
}
}
}

70
src/Squidex/Areas/Api/Config/Swagger/ErrorDtoProcessor.cs

@ -0,0 +1,70 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Linq;
using System.Threading.Tasks;
using NJsonSchema;
using NSwag;
using NSwag.SwaggerGeneration.Processors;
using NSwag.SwaggerGeneration.Processors.Contexts;
using Squidex.ClientLibrary.Management;
using Squidex.Pipeline.Swagger;
namespace Squidex.Areas.Api.Config.Swagger
{
public sealed class ErrorDtoProcessor : IDocumentProcessor
{
public async Task ProcessAsync(DocumentProcessorContext context)
{
var errorSchema = await GetErrorSchemaAsync(context);
foreach (var operation in context.Document.Paths.Values.SelectMany(x => x.Values))
{
AddErrorResponses(operation, errorSchema);
CleanupResponses(operation);
}
}
private static void AddErrorResponses(SwaggerOperation operation, JsonSchema4 errorSchema)
{
if (!operation.Responses.ContainsKey("500"))
{
operation.AddResponse("500", "Operation failed", errorSchema);
}
foreach (var (code, response) in operation.Responses)
{
if (code != "404" && code.StartsWith("4", StringComparison.OrdinalIgnoreCase) && response.Schema == null)
{
response.Schema = errorSchema;
}
}
}
private static void CleanupResponses(SwaggerOperation operation)
{
foreach (var (code, response) in operation.Responses.ToList())
{
if (string.IsNullOrWhiteSpace(response.Description) ||
response.Description?.Contains("=&gt;") == true ||
response.Description?.Contains("=>") == true)
{
operation.Responses.Remove(code);
}
}
}
private Task<JsonSchema4> GetErrorSchemaAsync(DocumentProcessorContext context)
{
var errorType = typeof(ErrorDto);
return context.SchemaGenerator.GenerateWithReferenceAsync<JsonSchema4>(errorType, Enumerable.Empty<Attribute>(), context.SchemaResolver);
}
}
}

2
src/Squidex/Areas/Api/Config/Swagger/FixProcessor.cs

@ -13,7 +13,7 @@ using Squidex.Infrastructure.Tasks;
namespace Squidex.Areas.Api.Config.Swagger
{
public class FixProcessor : IOperationProcessor
public sealed class FixProcessor : IOperationProcessor
{
private static readonly JsonSchema4 StringSchema = new JsonSchema4 { Type = JsonObjectType.String };

29
src/Squidex/Areas/Api/Config/Swagger/SecurityProcessor.cs

@ -15,7 +15,7 @@ using Squidex.Web;
namespace Squidex.Areas.Api.Config.Swagger
{
public class SecurityProcessor : SecurityDefinitionAppender
public sealed class SecurityProcessor : SecurityDefinitionAppender
{
public SecurityProcessor(IOptions<UrlsOptions> urlOptions)
: base(Constants.SecurityDefinition, Enumerable.Empty<string>(), CreateOAuthSchema(urlOptions.Value))
@ -24,26 +24,33 @@ namespace Squidex.Areas.Api.Config.Swagger
private static SwaggerSecurityScheme CreateOAuthSchema(UrlsOptions urlOptions)
{
var securityScheme = new SwaggerSecurityScheme();
var security = new SwaggerSecurityScheme
{
Type = SwaggerSecuritySchemeType.OAuth2
};
var tokenUrl = urlOptions.BuildUrl($"{Constants.IdentityServerPrefix}/connect/token", false);
securityScheme.TokenUrl = tokenUrl;
security.TokenUrl = tokenUrl;
var securityDocs = NSwagHelper.LoadDocs("security");
var securityText = securityDocs.Replace("<TOKEN_URL>", tokenUrl);
SetupDescription(security, tokenUrl);
securityScheme.Description = securityText;
securityScheme.Type = SwaggerSecuritySchemeType.OAuth2;
securityScheme.Flow = SwaggerOAuth2Flow.Application;
security.Flow = SwaggerOAuth2Flow.Application;
securityScheme.Scopes = new Dictionary<string, string>
security.Scopes = new Dictionary<string, string>
{
[Constants.ApiScope] = "Read and write access to the API"
};
return securityScheme;
return security;
}
private static void SetupDescription(SwaggerSecurityScheme securityScheme, string tokenUrl)
{
var securityDocs = NSwagHelper.LoadDocs("security");
var securityText = securityDocs.Replace("<TOKEN_URL>", tokenUrl);
securityScheme.Description = securityText;
}
}
}

3
src/Squidex/Areas/Api/Config/Swagger/SwaggerServices.cs

@ -22,6 +22,9 @@ namespace Squidex.Areas.Api.Config.Swagger
{
public static void AddMySwaggerSettings(this IServiceCollection services)
{
services.AddSingletonAs<ErrorDtoProcessor>()
.As<IDocumentProcessor>();
services.AddSingletonAs<RuleActionProcessor>()
.As<IDocumentProcessor>();

36
src/Squidex/Areas/Api/Config/Swagger/XmlResponseTypesProcessor.cs

@ -5,16 +5,13 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Linq;
using System;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using NJsonSchema.Infrastructure;
using NSwag;
using NSwag.SwaggerGeneration.Processors;
using NSwag.SwaggerGeneration.Processors.Contexts;
using Squidex.Pipeline.Swagger;
#pragma warning disable RECS0033 // Convert 'if' to '||' expression
namespace Squidex.Areas.Api.Config.Swagger
{
@ -26,8 +23,10 @@ namespace Squidex.Areas.Api.Config.Swagger
{
var operation = context.OperationDescription.Operation;
var returnsDescription = await context.MethodInfo.GetXmlDocumentationTagAsync("returns") ?? string.Empty;
var returnsDescription = await context.MethodInfo.GetXmlDocumentationTagAsync("returns");
if (!string.IsNullOrWhiteSpace(returnsDescription))
{
foreach (Match match in ResponseRegex.Matches(returnsDescription))
{
var statusCode = match.Groups["Code"].Value;
@ -39,33 +38,18 @@ namespace Squidex.Areas.Api.Config.Swagger
operation.Responses[statusCode] = response;
}
response.Description = match.Groups["Description"].Value;
}
await AddInternalErrorResponseAsync(context, operation);
CleanupResponses(operation);
return true;
}
var description = match.Groups["Description"].Value;
private static async Task AddInternalErrorResponseAsync(OperationProcessorContext context, SwaggerOperation operation)
if (description.Contains("=&gt;"))
{
if (!operation.Responses.ContainsKey("500"))
{
operation.AddResponse("500", "Operation failed", await context.SchemaGenerator.GetErrorDtoSchemaAsync(context.SchemaResolver));
}
throw new InvalidOperationException("Description not formatted correcly.");
}
private static void CleanupResponses(SwaggerOperation operation)
{
foreach (var (code, response) in operation.Responses.ToList())
{
if (string.IsNullOrWhiteSpace(response.Description) || response.Description?.Contains("=>") == true)
{
operation.Responses.Remove(code);
response.Description = description;
}
}
return true;
}
}
}

53
src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs

@ -5,11 +5,11 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
using Squidex.Areas.Api.Controllers.Apps.Models;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure.Commands;
using Squidex.Shared;
@ -41,12 +41,12 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// </remarks>
[HttpGet]
[Route("apps/{app}/clients/")]
[ProducesResponseType(typeof(ClientDto[]), 200)]
[ProducesResponseType(typeof(ClientsDto), 200)]
[ApiPermission(Permissions.AppClientsRead)]
[ApiCosts(0)]
public IActionResult GetClients(string app)
{
var response = App.Clients.Select(ClientDto.FromKvp).ToArray();
var response = ClientsDto.FromApp(App, this);
Response.Headers[HeaderNames.ETag] = App.Version.ToString();
@ -60,6 +60,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// <param name="request">Client object that needs to be added to the app.</param>
/// <returns>
/// 201 => Client generated.
/// 400 => Client request not valid.
/// 404 => App not found.
/// </returns>
/// <remarks>
@ -68,16 +69,14 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// </remarks>
[HttpPost]
[Route("apps/{app}/clients/")]
[ProducesResponseType(typeof(ClientDto), 201)]
[ProducesResponseType(typeof(ClientsDto), 200)]
[ApiPermission(Permissions.AppClientsCreate)]
[ApiCosts(1)]
public async Task<IActionResult> PostClient(string app, [FromBody] CreateClientDto request)
{
var command = request.ToCommand();
await CommandBus.PublishAsync(command);
var response = ClientDto.FromCommand(command);
var response = await InvokeCommandAsync(command);
return CreatedAtAction(nameof(GetClients), new { app }, response);
}
@ -86,10 +85,10 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// Updates an app client.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="clientId">The id of the client that must be updated.</param>
/// <param name="id">The id of the client that must be updated.</param>
/// <param name="request">Client object that needs to be updated.</param>
/// <returns>
/// 204 => Client updated.
/// 200 => Client updated.
/// 400 => Client request not valid.
/// 404 => Client or app not found.
/// </returns>
@ -97,37 +96,53 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// Only the display name can be changed, create a new client if necessary.
/// </remarks>
[HttpPut]
[Route("apps/{app}/clients/{clientId}/")]
[Route("apps/{app}/clients/{id}/")]
[ProducesResponseType(typeof(ClientsDto), 200)]
[ApiPermission(Permissions.AppClientsUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> PutClient(string app, string clientId, [FromBody] UpdateClientDto request)
public async Task<IActionResult> PutClient(string app, string id, [FromBody] UpdateClientDto request)
{
await CommandBus.PublishAsync(request.ToCommand(clientId));
var command = request.ToCommand(id);
var response = await InvokeCommandAsync(command);
return NoContent();
return Ok(response);
}
/// <summary>
/// Revoke an app client.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="clientId">The id of the client that must be deleted.</param>
/// <param name="id">The id of the client that must be deleted.</param>
/// <returns>
/// 204 => Client revoked.
/// 200 => Client revoked.
/// 404 => Client or app not found.
/// </returns>
/// <remarks>
/// The application that uses this client credentials cannot access the API after it has been revoked.
/// </remarks>
[HttpDelete]
[Route("apps/{app}/clients/{clientId}/")]
[Route("apps/{app}/clients/{id}/")]
[ProducesResponseType(typeof(ClientsDto), 200)]
[ApiPermission(Permissions.AppClientsDelete)]
[ApiCosts(1)]
public async Task<IActionResult> DeleteClient(string app, string clientId)
public async Task<IActionResult> DeleteClient(string app, string id)
{
await CommandBus.PublishAsync(new RevokeClient { Id = clientId });
var command = new RevokeClient { Id = id };
var response = await InvokeCommandAsync(command);
return Ok(response);
}
private async Task<ClientsDto> InvokeCommandAsync(ICommand command)
{
var context = await CommandBus.PublishAsync(command);
var result = context.Result<IAppEntity>();
var response = ClientsDto.FromApp(result, this);
return NoContent();
return response;
}
}
}

38
src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs

@ -9,6 +9,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
using Squidex.Areas.Api.Controllers.Apps.Models;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Domain.Apps.Entities.Apps.Invitation;
using Squidex.Domain.Apps.Entities.Apps.Services;
@ -47,7 +48,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
[ApiCosts(0)]
public IActionResult GetContributors(string app)
{
var response = ContributorsDto.FromApp(App, appPlansProvider);
var response = ContributorsDto.FromApp(App, appPlansProvider, this, false);
Response.Headers[HeaderNames.ETag] = App.Version.ToString();
@ -60,14 +61,13 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// <param name="app">The name of the app.</param>
/// <param name="request">Contributor object that needs to be added to the app.</param>
/// <returns>
/// 200 => User assigned to app.
/// 201 => User assigned to app.
/// 400 => User is already assigned to the app or not found.
/// 404 => App not found.
/// </returns>
[HttpPost]
[Route("apps/{app}/contributors/")]
[ProducesResponseType(typeof(ContributorAssignedDto), 201)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ProducesResponseType(typeof(ContributorsDto), 200)]
[ApiPermission(Permissions.AppContributorsAssign)]
[ApiCosts(1)]
public async Task<IActionResult> PostContributor(string app, [FromBody] AssignContributorDto request)
@ -75,18 +75,18 @@ namespace Squidex.Areas.Api.Controllers.Apps
var command = request.ToCommand();
var context = await CommandBus.PublishAsync(command);
var response = (ContributorAssignedDto)null;
var response = (ContributorsDto)null;
if (context.PlainResult is EntityCreatedResult<string> idOrValue)
if (context.PlainResult is IAppEntity newApp)
{
response = ContributorAssignedDto.FromId(idOrValue.IdOrValue, false);
response = ContributorsDto.FromApp(newApp, appPlansProvider, this, false);
}
else if (context.PlainResult is InvitedResult invited)
{
response = ContributorAssignedDto.FromId(invited.Id.IdOrValue, true);
response = ContributorsDto.FromApp(invited.App, appPlansProvider, this, true);
}
return Ok(response);
return CreatedAtAction(nameof(GetContributors), new { app }, response);
}
/// <summary>
@ -95,20 +95,32 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// <param name="app">The name of the app.</param>
/// <param name="id">The id of the contributor.</param>
/// <returns>
/// 204 => User removed from app.
/// 200 => User removed from app.
/// 400 => User is not assigned to the app.
/// 404 => Contributor or app not found.
/// </returns>
[HttpDelete]
[Route("apps/{app}/contributors/{id}/")]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ProducesResponseType(typeof(ContributorsDto), 200)]
[ApiPermission(Permissions.AppContributorsRevoke)]
[ApiCosts(1)]
public async Task<IActionResult> DeleteContributor(string app, string id)
{
await CommandBus.PublishAsync(new RemoveContributor { ContributorId = id });
var command = new RemoveContributor { ContributorId = id };
var response = await InvokeCommandAsync(command);
return Ok(response);
}
private async Task<ContributorsDto> InvokeCommandAsync(ICommand command)
{
var context = await CommandBus.PublishAsync(command);
var result = context.Result<IAppEntity>();
var response = ContributorsDto.FromApp(result, appPlansProvider, this, false);
return NoContent();
return response;
}
}
}

42
src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs

@ -10,6 +10,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
using Squidex.Areas.Api.Controllers.Apps.Models;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
@ -39,12 +40,12 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// </returns>
[HttpGet]
[Route("apps/{app}/languages/")]
[ProducesResponseType(typeof(AppLanguageDto[]), 200)]
[ProducesResponseType(typeof(AppLanguagesDto), 200)]
[ApiPermission(Permissions.AppCommon)]
[ApiCosts(0)]
public IActionResult GetLanguages(string app)
{
var response = AppLanguageDto.FromApp(App);
var response = AppLanguagesDto.FromApp(App, this);
Response.Headers[HeaderNames.ETag] = App.Version.ToString();
@ -63,17 +64,14 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// </returns>
[HttpPost]
[Route("apps/{app}/languages/")]
[ProducesResponseType(typeof(AppLanguageDto), 201)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ProducesResponseType(typeof(AppLanguagesDto), 201)]
[ApiPermission(Permissions.AppLanguagesCreate)]
[ApiCosts(1)]
public async Task<IActionResult> PostLanguage(string app, [FromBody] AddLanguageDto request)
{
var command = request.ToCommand();
await CommandBus.PublishAsync(command);
var response = AppLanguageDto.FromCommand(command);
var response = await InvokeCommandAsync(command);
return CreatedAtAction(nameof(GetLanguages), new { app }, response);
}
@ -85,19 +83,22 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// <param name="language">The language to update.</param>
/// <param name="request">The language object.</param>
/// <returns>
/// 204 => Language updated.
/// 200 => Language updated.
/// 400 => Language request not valid.
/// 404 => Language or app not found.
/// </returns>
[HttpPut]
[Route("apps/{app}/languages/{language}/")]
[ProducesResponseType(typeof(AppLanguagesDto), 200)]
[ApiPermission(Permissions.AppLanguagesUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> Update(string app, string language, [FromBody] UpdateLanguageDto request)
public async Task<IActionResult> PutLanguage(string app, string language, [FromBody] UpdateLanguageDto request)
{
await CommandBus.PublishAsync(request.ToCommand(ParseLanguage(language)));
var command = request.ToCommand(ParseLanguage(language));
var response = await InvokeCommandAsync(command);
return NoContent();
return Ok(response);
}
/// <summary>
@ -106,18 +107,31 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// <param name="app">The name of the app.</param>
/// <param name="language">The language to delete from the app.</param>
/// <returns>
/// 204 => Language deleted.
/// 200 => Language deleted.
/// 404 => Language or app not found.
/// </returns>
[HttpDelete]
[Route("apps/{app}/languages/{language}/")]
[ProducesResponseType(typeof(AppLanguagesDto), 200)]
[ApiPermission(Permissions.AppLanguagesDelete)]
[ApiCosts(1)]
public async Task<IActionResult> DeleteLanguage(string app, string language)
{
await CommandBus.PublishAsync(new RemoveLanguage { Language = ParseLanguage(language) });
var command = new RemoveLanguage { Language = ParseLanguage(language) };
var response = await InvokeCommandAsync(command);
return Ok(response);
}
private async Task<AppLanguagesDto> InvokeCommandAsync(ICommand command)
{
var context = await CommandBus.PublishAsync(command);
var result = context.Result<IAppEntity>();
var response = AppLanguagesDto.FromApp(result, this);
return NoContent();
return response;
}
private static Language ParseLanguage(string language)

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

@ -6,11 +6,11 @@
// ==========================================================================
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
using Squidex.Areas.Api.Controllers.Apps.Models;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure.Commands;
using Squidex.Shared;
@ -42,12 +42,12 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// </remarks>
[HttpGet]
[Route("apps/{app}/patterns/")]
[ProducesResponseType(typeof(AppPatternDto[]), 200)]
[ApiPermission(Permissions.AppPatternsRead)]
[ProducesResponseType(typeof(PatternsDto), 200)]
[ApiPermission(Permissions.AppCommon)]
[ApiCosts(0)]
public IActionResult GetPatterns(string app)
{
var response = App.Patterns.Select(AppPatternDto.FromKvp).OrderBy(x => x.Name).ToArray();
var response = PatternsDto.FromApp(App, this);
Response.Headers[HeaderNames.ETag] = App.Version.ToString();
@ -66,16 +66,14 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// </returns>
[HttpPost]
[Route("apps/{app}/patterns/")]
[ProducesResponseType(typeof(AppPatternDto), 201)]
[ProducesResponseType(typeof(PatternsDto), 200)]
[ApiPermission(Permissions.AppPatternsCreate)]
[ApiCosts(1)]
public async Task<IActionResult> PostPattern(string app, [FromBody] UpdatePatternDto request)
{
var command = request.ToAddCommand();
await CommandBus.PublishAsync(command);
var response = AppPatternDto.FromCommand(command);
var response = await InvokeCommandAsync(command);
return CreatedAtAction(nameof(GetPatterns), new { app }, response);
}
@ -87,20 +85,22 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// <param name="id">The id of the pattern to be updated.</param>
/// <param name="request">Pattern to be updated for the app.</param>
/// <returns>
/// 204 => Pattern updated.
/// 200 => Pattern updated.
/// 400 => Pattern request not valid.
/// 404 => Pattern or app not found.
/// </returns>
[HttpPut]
[Route("apps/{app}/patterns/{id}/")]
[ProducesResponseType(typeof(AppPatternDto), 201)]
[ProducesResponseType(typeof(PatternsDto), 200)]
[ApiPermission(Permissions.AppPatternsUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> UpdatePattern(string app, Guid id, [FromBody] UpdatePatternDto request)
{
await CommandBus.PublishAsync(request.ToUpdateCommand(id));
var command = request.ToUpdateCommand(id);
var response = await InvokeCommandAsync(command);
return NoContent();
return Ok(response);
}
/// <summary>
@ -109,7 +109,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// <param name="app">The name of the app.</param>
/// <param name="id">The id of the pattern to be deleted.</param>
/// <returns>
/// 204 => Pattern removed.
/// 200 => Pattern removed.
/// 404 => Pattern or app not found.
/// </returns>
/// <remarks>
@ -117,13 +117,26 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// </remarks>
[HttpDelete]
[Route("apps/{app}/patterns/{id}/")]
[ProducesResponseType(typeof(PatternsDto), 200)]
[ApiPermission(Permissions.AppPatternsDelete)]
[ApiCosts(1)]
public async Task<IActionResult> DeletePattern(string app, Guid id)
{
await CommandBus.PublishAsync(new DeletePattern { PatternId = id });
var command = new DeletePattern { PatternId = id };
var response = await InvokeCommandAsync(command);
return Ok(response);
}
private async Task<PatternsDto> InvokeCommandAsync(ICommand command)
{
var context = await CommandBus.PublishAsync(command);
var result = context.Result<IAppEntity>();
var response = PatternsDto.FromApp(result, this);
return NoContent();
return response;
}
}
}

53
src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs

@ -47,7 +47,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
[ApiCosts(0)]
public IActionResult GetRoles(string app)
{
var response = RolesDto.FromApp(App);
var response = RolesDto.FromApp(App, this);
Response.Headers[HeaderNames.ETag] = App.Version.ToString();
@ -82,64 +82,81 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// <param name="app">The name of the app.</param>
/// <param name="request">Role object that needs to be added to the app.</param>
/// <returns>
/// 200 => User assigned to app.
/// 201 => User assigned to app.
/// 400 => Role name already in use.
/// 404 => App not found.
/// </returns>
[HttpPost]
[Route("apps/{app}/roles/")]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ProducesResponseType(typeof(RolesDto), 200)]
[ApiPermission(Permissions.AppRolesCreate)]
[ApiCosts(1)]
public async Task<IActionResult> PostRole(string app, [FromBody] AddRoleDto request)
{
await CommandBus.PublishAsync(request.ToCommand());
var command = request.ToCommand();
var response = await InvokeCommandAsync(command);
return NoContent();
return CreatedAtAction(nameof(GetRoles), new { app }, response);
}
/// <summary>
/// Update an existing app role.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="role">The name of the role to be updated.</param>
/// <param name="name">The name of the role to be updated.</param>
/// <param name="request">Role to be updated for the app.</param>
/// <returns>
/// 204 => Role updated.
/// 200 => Role updated.
/// 400 => Role request not valid.
/// 404 => Role or app not found.
/// </returns>
[HttpPut]
[Route("apps/{app}/roles/{role}/")]
[Route("apps/{app}/roles/{name}/")]
[ProducesResponseType(typeof(RolesDto), 200)]
[ApiPermission(Permissions.AppRolesUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> UpdateRole(string app, string role, [FromBody] UpdateRoleDto request)
public async Task<IActionResult> UpdateRole(string app, string name, [FromBody] UpdateRoleDto request)
{
await CommandBus.PublishAsync(request.ToCommand(role));
var command = request.ToCommand(name);
var response = await InvokeCommandAsync(command);
return NoContent();
return Ok(response);
}
/// <summary>
/// Remove role from app.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="role">The name of the role.</param>
/// <param name="name">The name of the role.</param>
/// <returns>
/// 204 => Role deleted.
/// 200 => Role deleted.
/// 400 => Role is in use by contributor or client or default role.
/// 404 => Role or app not found.
/// </returns>
[HttpDelete]
[Route("apps/{app}/roles/{role}/")]
[ProducesResponseType(typeof(ErrorDto), 400)]
[Route("apps/{app}/roles/{name}/")]
[ProducesResponseType(typeof(RolesDto), 200)]
[ApiPermission(Permissions.AppRolesDelete)]
[ApiCosts(1)]
public async Task<IActionResult> DeleteRole(string app, string role)
public async Task<IActionResult> DeleteRole(string app, string name)
{
await CommandBus.PublishAsync(new DeleteRole { Name = role });
var command = new DeleteRole { Name = name };
var response = await InvokeCommandAsync(command);
return Ok(response);
}
private async Task<RolesDto> InvokeCommandAsync(ICommand command)
{
var context = await CommandBus.PublishAsync(command);
var result = context.Result<IAppEntity>();
var response = RolesDto.FromApp(result, this);
return NoContent();
return response;
}
}
}

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

@ -5,12 +5,12 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
using Squidex.Areas.Api.Controllers.Apps.Models;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Domain.Apps.Entities.Apps.Services;
using Squidex.Infrastructure;
@ -58,11 +58,11 @@ namespace Squidex.Areas.Api.Controllers.Apps
public async Task<IActionResult> GetApps()
{
var userOrClientId = HttpContext.User.UserOrClientId();
var userPermissions = HttpContext.User.Permissions();
var userPermissions = HttpContext.Permissions();
var entities = await appProvider.GetUserApps(userOrClientId, userPermissions);
var apps = await appProvider.GetUserApps(userOrClientId, userPermissions);
var response = entities.ToArray(a => AppDto.FromApp(a, userOrClientId, userPermissions, appPlansProvider));
var response = apps.ToArray(a => AppDto.FromApp(a, userOrClientId, userPermissions, appPlansProvider, this));
Response.Headers[HeaderNames.ETag] = response.ToManyEtag();
@ -84,17 +84,18 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// </remarks>
[HttpPost]
[Route("apps/")]
[ProducesResponseType(typeof(AppCreatedDto), 201)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ProducesResponseType(typeof(ErrorDto), 409)]
[ProducesResponseType(typeof(AppDto), 201)]
[ApiPermission]
[ApiCosts(1)]
public async Task<IActionResult> PostApp([FromBody] CreateAppDto request)
{
var context = await CommandBus.PublishAsync(request.ToCommand());
var result = context.Result<EntityCreatedResult<Guid>>();
var response = AppCreatedDto.FromResult(request.Name, result, appPlansProvider);
var userOrClientId = HttpContext.User.UserOrClientId();
var userPermissions = HttpContext.Permissions();
var result = context.Result<IAppEntity>();
var response = AppDto.FromApp(result, userOrClientId, userPermissions, appPlansProvider, this);
return CreatedAtAction(nameof(GetApps), response);
}

59
src/Squidex/Areas/Api/Controllers/Apps/Models/AppCreatedDto.cs

@ -1,59 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps.Services;
using Squidex.Infrastructure.Commands;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
public sealed class AppCreatedDto
{
/// <summary>
/// Id of the created entity.
/// </summary>
[Required]
public string Id { get; set; }
/// <summary>
/// The permission level of the user.
/// </summary>
public string[] Permissions { get; set; }
/// <summary>
/// The new version of the entity.
/// </summary>
public long Version { get; set; }
/// <summary>
/// Gets the current plan name.
/// </summary>
public string PlanName { get; set; }
/// <summary>
/// Gets the next plan name.
/// </summary>
public string PlanUpgrade { get; set; }
public static AppCreatedDto FromResult(string name, EntityCreatedResult<Guid> result, IAppPlansProvider apps)
{
var response = new AppCreatedDto
{
Id = result.IdOrValue.ToString(),
Permissions = Role.CreateOwner(name).Permissions.ToIds().ToArray(),
PlanName = apps.GetPlan(null)?.Name,
PlanUpgrade = apps.GetPlanUpgrade(null)?.Name,
Version = result.Version
};
return response;
}
}
}

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

@ -9,17 +9,23 @@ using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using NodaTime;
using Squidex.Areas.Api.Controllers.Assets;
using Squidex.Areas.Api.Controllers.Backups;
using Squidex.Areas.Api.Controllers.Ping;
using Squidex.Areas.Api.Controllers.Plans;
using Squidex.Areas.Api.Controllers.Rules;
using Squidex.Areas.Api.Controllers.Schemas;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.Services;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.Security;
using Squidex.Shared;
using Squidex.Web;
using AllPermissions = Squidex.Shared.Permissions;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
public sealed class AppDto : IGenerateETag
public sealed class AppDto : Resource, IGenerateETag
{
/// <summary>
/// The name of the app.
@ -51,7 +57,17 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// <summary>
/// The permission level of the user.
/// </summary>
public string[] Permissions { get; set; }
public IEnumerable<string> Permissions { get; set; }
/// <summary>
/// Indicates if the user can access the api.
/// </summary>
public bool CanAccessApi { get; set; }
/// <summary>
/// Indicates if the user can access at least one content.
/// </summary>
public bool CanAccessContent { get; set; }
/// <summary>
/// Gets the current plan name.
@ -63,7 +79,27 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// </summary>
public string PlanUpgrade { get; set; }
public static AppDto FromApp(IAppEntity app, string userId, PermissionSet userPermissions, IAppPlansProvider plans)
public static AppDto FromApp(IAppEntity app, string userId, PermissionSet userPermissions, IAppPlansProvider plans, ApiController controller)
{
var permissions = GetPermissions(app, userId, userPermissions);
var result = SimpleMapper.Map(app, new AppDto());
result.Permissions = permissions.ToIds();
result.PlanName = plans.GetPlanForApp(app)?.Name;
result.CanAccessApi = controller.HasPermission(AllPermissions.AppApi, app.Name, "*", permissions);
result.CanAccessContent = controller.HasPermission(AllPermissions.AppContentsRead, app.Name, "*", permissions);
if (controller.HasPermission(AllPermissions.AppPlansChange, app.Name))
{
result.PlanUpgrade = plans.GetPlanUpgradeForApp(app)?.Name;
}
return result.CreateLinks(controller, permissions);
}
private static PermissionSet GetPermissions(IAppEntity app, string userId, PermissionSet userPermissions)
{
var permissions = new List<Permission>();
@ -77,13 +113,81 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
permissions.AddRange(userPermissions.ToAppPermissions(app.Name));
}
var response = SimpleMapper.Map(app, new AppDto());
return new PermissionSet(permissions);
}
private AppDto CreateLinks(ApiController controller, PermissionSet permissions)
{
var values = new { app = Name };
AddGetLink("ping", controller.Url<PingController>(x => nameof(x.GetAppPing), values));
if (controller.HasPermission(AllPermissions.AppDelete, Name, permissions: permissions))
{
AddDeleteLink("delete", controller.Url<AppsController>(x => nameof(x.DeleteApp), values));
}
if (controller.HasPermission(AllPermissions.AppAssetsRead, Name, permissions: permissions))
{
AddGetLink("assets", controller.Url<AssetsController>(x => nameof(x.GetAssets), values));
}
if (controller.HasPermission(AllPermissions.AppBackupsRead, Name, permissions: permissions))
{
AddGetLink("backups", controller.Url<BackupsController>(x => nameof(x.GetBackups), values));
}
if (controller.HasPermission(AllPermissions.AppClientsRead, Name, permissions: permissions))
{
AddGetLink("clients", controller.Url<AppClientsController>(x => nameof(x.GetClients), values));
}
if (controller.HasPermission(AllPermissions.AppContributorsRead, Name, permissions: permissions))
{
AddGetLink("contributors", controller.Url<AppContributorsController>(x => nameof(x.GetContributors), values));
}
if (controller.HasPermission(AllPermissions.AppCommon, Name, permissions: permissions))
{
AddGetLink("languages", controller.Url<AppLanguagesController>(x => nameof(x.GetLanguages), values));
}
if (controller.HasPermission(AllPermissions.AppCommon, Name, permissions: permissions))
{
AddGetLink("patterns", controller.Url<AppPatternsController>(x => nameof(x.GetPatterns), values));
}
if (controller.HasPermission(AllPermissions.AppPlansRead, Name, permissions: permissions))
{
AddGetLink("plans", controller.Url<AppPlansController>(x => nameof(x.GetPlans), values));
}
response.Permissions = permissions.ToArray(x => x.Id);
response.PlanName = plans.GetPlanForApp(app)?.Name;
response.PlanUpgrade = plans.GetPlanUpgradeForApp(app)?.Name;
if (controller.HasPermission(AllPermissions.AppRolesRead, Name, permissions: permissions))
{
AddGetLink("roles", controller.Url<AppRolesController>(x => nameof(x.GetRoles), values));
}
if (controller.HasPermission(AllPermissions.AppRulesRead, Name, permissions: permissions))
{
AddGetLink("rules", controller.Url<RulesController>(x => nameof(x.GetRules), values));
}
if (controller.HasPermission(AllPermissions.AppCommon, Name, permissions: permissions))
{
AddGetLink("schemas", controller.Url<SchemasController>(x => nameof(x.GetSchemas), values));
}
if (controller.HasPermission(AllPermissions.AppSchemasCreate, Name, permissions: permissions))
{
AddPostLink("schemas/create", controller.Url<SchemasController>(x => nameof(x.PostSchema), values));
}
if (controller.HasPermission(AllPermissions.AppAssetsCreate, Name, permissions: permissions))
{
AddPostLink("assets/create", controller.Url<SchemasController>(x => nameof(x.PostSchema), values));
}
return response;
return this;
}
}
}

42
src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguageDto.cs

@ -5,18 +5,18 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Reflection;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
public sealed class AppLanguageDto
public sealed class AppLanguageDto : Resource
{
/// <summary>
/// The iso code of the language.
@ -46,25 +46,37 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// </summary>
public bool IsOptional { get; set; }
public static AppLanguageDto FromCommand(AddLanguage command)
public static AppLanguageDto FromLanguage(LanguageConfig language, IAppEntity app, ApiController controller)
{
return SimpleMapper.Map(command.Language, new AppLanguageDto { Fallback = Array.Empty<Language>() });
var result = SimpleMapper.Map(language.Language,
new AppLanguageDto
{
IsMaster = language == app.LanguagesConfig.Master,
IsOptional = language.IsOptional,
Fallback = language.LanguageFallbacks.ToArray()
});
return result.CreateLinks(controller, app);
}
public static AppLanguageDto[] FromApp(IAppEntity app)
private AppLanguageDto CreateLinks(ApiController controller, IAppEntity app)
{
return app.LanguagesConfig.OfType<LanguageConfig>().Select(x => FromLanguage(x, app)).OrderByDescending(x => x.IsMaster).ThenBy(x => x.Iso2Code).ToArray();
}
var values = new { app = app.Name, language = Iso2Code };
private static AppLanguageDto FromLanguage(LanguageConfig x, IAppEntity app)
if (!IsMaster)
{
return SimpleMapper.Map(x.Language,
new AppLanguageDto
if (controller.HasPermission(Permissions.AppLanguagesUpdate, app.Name))
{
IsMaster = x == app.LanguagesConfig.Master,
IsOptional = x.IsOptional,
Fallback = x.LanguageFallbacks.ToArray()
});
AddPutLink("update", controller.Url<AppLanguagesController>(x => nameof(x.PutLanguage), values));
}
if (controller.HasPermission(Permissions.AppLanguagesDelete, app.Name) && app.LanguagesConfig.Count > 1)
{
AddDeleteLink("delete", controller.Url<AppLanguagesController>(x => nameof(x.DeleteLanguage), values));
}
}
return this;
}
}
}

53
src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguagesDto.cs

@ -0,0 +1,53 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
public sealed class AppLanguagesDto : Resource
{
/// <summary>
/// The languages.
/// </summary>
[Required]
public AppLanguageDto[] Items { get; set; }
public static AppLanguagesDto FromApp(IAppEntity app, ApiController controller)
{
var result = new AppLanguagesDto
{
Items = app.LanguagesConfig.OfType<LanguageConfig>()
.Select(x => AppLanguageDto.FromLanguage(x, app, controller))
.OrderByDescending(x => x.IsMaster)
.ThenBy(x => x.Iso2Code)
.ToArray()
};
return result.CreateLinks(controller, app.Name);
}
private AppLanguagesDto CreateLinks(ApiController controller, string app)
{
var values = new { app };
AddSelfLink(controller.Url<AppLanguagesController>(x => nameof(x.GetLanguages), values));
if (controller.HasPermission(Permissions.AppLanguagesCreate, app))
{
AddPostLink("create", controller.Url<AppLanguagesController>(x => nameof(x.PostLanguage), values));
}
return this;
}
}
}

29
src/Squidex/Areas/Api/Controllers/Apps/Models/ClientDto.cs

@ -5,16 +5,15 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure.Reflection;
using Roles = Squidex.Domain.Apps.Core.Apps.Role;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
public sealed class ClientDto
public sealed class ClientDto : Resource
{
/// <summary>
/// The client id.
@ -39,14 +38,28 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// </summary>
public string Role { get; set; }
public static ClientDto FromKvp(KeyValuePair<string, AppClient> kvp)
public static ClientDto FromClient(string id, AppClient client, ApiController controller, string app)
{
return SimpleMapper.Map(kvp.Value, new ClientDto { Id = kvp.Key });
var result = SimpleMapper.Map(client, new ClientDto { Id = id });
return result.CreateLinks(controller, app);
}
private ClientDto CreateLinks(ApiController controller, string app)
{
var values = new { app, id = Id };
if (controller.HasPermission(Permissions.AppClientsUpdate, app))
{
AddPutLink("update", controller.Url<AppClientsController>(x => nameof(x.PutClient), values));
}
public static ClientDto FromCommand(AttachClient command)
if (controller.HasPermission(Permissions.AppClientsDelete, app))
{
return SimpleMapper.Map(command, new ClientDto { Name = command.Id, Role = Roles.Editor });
AddDeleteLink("delete", controller.Url<AppClientsController>(x => nameof(x.DeleteClient), values));
}
return this;
}
}
}

48
src/Squidex/Areas/Api/Controllers/Apps/Models/ClientsDto.cs

@ -0,0 +1,48 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
public sealed class ClientsDto : Resource
{
/// <summary>
/// The clients.
/// </summary>
[Required]
public ClientDto[] Items { get; set; }
public static ClientsDto FromApp(IAppEntity app, ApiController controller)
{
var result = new ClientsDto
{
Items = app.Clients.Select(x => ClientDto.FromClient(x.Key, x.Value, controller, app.Name)).ToArray()
};
return result.CreateLinks(controller, app.Name);
}
private ClientsDto CreateLinks(ApiController controller, string app)
{
var values = new { app };
AddSelfLink(controller.Url<AppClientsController>(x => nameof(x.GetClients), values));
if (controller.HasPermission(Permissions.AppClientsCreate, app))
{
AddPostLink("create", controller.Url<AppClientsController>(x => nameof(x.PostClient), values));
}
return this;
}
}
}

29
src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorDto.cs

@ -6,10 +6,12 @@
// ==========================================================================
using System.ComponentModel.DataAnnotations;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
public sealed class ContributorDto
public sealed class ContributorDto : Resource
{
/// <summary>
/// The id of the user that contributes to the app.
@ -21,5 +23,30 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// The role of the contributor.
/// </summary>
public string Role { get; set; }
public static ContributorDto FromIdAndRole(string id, string role, ApiController controller, string app)
{
var result = new ContributorDto { ContributorId = id, Role = role };
return result.CreateLinks(controller, app);
}
private ContributorDto CreateLinks(ApiController controller, string app)
{
if (!controller.IsUser(ContributorId))
{
if (controller.HasPermission(Permissions.AppContributorsAssign, app))
{
AddPostLink("update", controller.Url<AppContributorsController>(x => nameof(x.PostContributor), new { app }));
}
if (controller.HasPermission(Permissions.AppContributorsRevoke, app))
{
AddDeleteLink("delete", controller.Url<AppContributorsController>(x => nameof(x.DeleteContributor), new { app, id = ContributorId }));
}
}
return this;
}
}
}

48
src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsDto.cs

@ -6,32 +6,68 @@
// ==========================================================================
using System.ComponentModel.DataAnnotations;
using Newtonsoft.Json;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.Services;
using Squidex.Infrastructure;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
public sealed class ContributorsDto
public sealed class ContributorsDto : Resource
{
/// <summary>
/// The contributors.
/// </summary>
[Required]
public ContributorDto[] Contributors { get; set; }
public ContributorDto[] Items { get; set; }
/// <summary>
/// The maximum number of allowed contributors.
/// </summary>
public int MaxContributors { get; set; }
public static ContributorsDto FromApp(IAppEntity app, IAppPlansProvider plans)
/// <summary>
/// The metadata.
/// </summary>
[JsonProperty("_meta")]
public ContributorsMetadata Metadata { get; set; }
public static ContributorsDto FromApp(IAppEntity app, IAppPlansProvider plans, ApiController controller, bool isInvited)
{
var contributors = app.Contributors.ToArray(x => ContributorDto.FromIdAndRole(x.Key, x.Value, controller, app.Name));
var result = new ContributorsDto
{
var plan = plans.GetPlanForApp(app);
Items = contributors,
};
if (isInvited)
{
result.Metadata = new ContributorsMetadata
{
IsInvited = isInvited.ToString()
};
}
var contributors = app.Contributors.ToArray(x => new ContributorDto { ContributorId = x.Key, Role = x.Value });
result.MaxContributors = plans.GetPlanForApp(app).MaxContributors;
return result.CreateLinks(controller, app.Name);
}
private ContributorsDto CreateLinks(ApiController controller, string app)
{
var values = new { app };
AddSelfLink(controller.Url<AppContributorsController>(x => nameof(x.GetContributors), values));
if (controller.HasPermission(Permissions.AppContributorsAssign, app) && (MaxContributors < 0 || Items.Length < MaxContributors))
{
AddPostLink("create", controller.Url<AppContributorsController>(x => nameof(x.PostContributor), values));
}
return new ContributorsDto { Contributors = contributors, MaxContributors = plan.MaxContributors };
return this;
}
}
}

17
src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsMetadata.cs

@ -0,0 +1,17 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
public sealed class ContributorsMetadata
{
/// <summary>
/// Indicates whether the user has been invited.
/// </summary>
public string IsInvited { get; set; }
}
}

30
src/Squidex/Areas/Api/Controllers/Apps/Models/AppPatternDto.cs → src/Squidex/Areas/Api/Controllers/Apps/Models/PatternDto.cs

@ -6,20 +6,20 @@
// ==========================================================================
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure.Reflection;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
public sealed class AppPatternDto
public sealed class PatternDto : Resource
{
/// <summary>
/// Unique id of the pattern.
/// </summary>
public Guid PatternId { get; set; }
public Guid Id { get; set; }
/// <summary>
/// The name of the suggestion.
@ -38,14 +38,28 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// </summary>
public string Message { get; set; }
public static AppPatternDto FromKvp(KeyValuePair<Guid, AppPattern> kvp)
public static PatternDto FromPattern(Guid id, AppPattern pattern, ApiController controller, string app)
{
return SimpleMapper.Map(kvp.Value, new AppPatternDto { PatternId = kvp.Key });
var result = SimpleMapper.Map(pattern, new PatternDto { Id = id });
return result.CreateLinks(controller, app);
}
private PatternDto CreateLinks(ApiController controller, string app)
{
var values = new { app, id = Id };
if (controller.HasPermission(Permissions.AppPatternsUpdate, app))
{
AddPutLink("update", controller.Url<AppPatternsController>(x => nameof(x.UpdatePattern), values));
}
public static AppPatternDto FromCommand(AddPattern command)
if (controller.HasPermission(Permissions.AppPatternsDelete, app))
{
return SimpleMapper.Map(command, new AppPatternDto());
AddDeleteLink("delete", controller.Url<AppPatternsController>(x => nameof(x.DeletePattern), values));
}
return this;
}
}
}

48
src/Squidex/Areas/Api/Controllers/Apps/Models/PatternsDto.cs

@ -0,0 +1,48 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
public sealed class PatternsDto : Resource
{
/// <summary>
/// The patterns.
/// </summary>
[Required]
public PatternDto[] Items { get; set; }
public static PatternsDto FromApp(IAppEntity app, ApiController controller)
{
var result = new PatternsDto
{
Items = app.Patterns.Select(x => PatternDto.FromPattern(x.Key, x.Value, controller, app.Name)).ToArray()
};
return result.CreateLinks(controller, app.Name);
}
private PatternsDto CreateLinks(ApiController controller, string app)
{
var values = new { app };
AddSelfLink(controller.Url<AppPatternsController>(x => nameof(x.GetPatterns), values));
if (controller.HasPermission(Permissions.AppPatternsCreate, app))
{
AddPostLink("create", controller.Url<AppPatternsController>(x => nameof(x.PostPattern), values));
}
return this;
}
}
}

43
src/Squidex/Areas/Api/Controllers/Apps/Models/RoleDto.cs

@ -5,16 +5,17 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Web;
using AllPermissions = Squidex.Shared.Permissions;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
public sealed class RoleDto
public sealed class RoleDto : Resource
{
/// <summary>
/// The role name.
@ -32,23 +33,51 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// </summary>
public int NumContributors { get; set; }
/// <summary>
/// Indicates if the role is an builtin default role.
/// </summary>
public bool IsDefaultRole { get; set; }
/// <summary>
/// Associated list of permissions.
/// </summary>
[Required]
public IEnumerable<string> Permissions { get; set; }
public static RoleDto FromRole(Role role, IAppEntity app)
public static RoleDto FromRole(Role role, IAppEntity app, ApiController controller)
{
var permissions = role.Permissions.WithoutApp(app.Name);
return new RoleDto
var result = new RoleDto
{
Name = role.Name,
NumClients = app.Clients.Count(x => string.Equals(x.Value.Role, role.Name, StringComparison.OrdinalIgnoreCase)),
NumContributors = app.Contributors.Count(x => string.Equals(x.Value, role.Name, StringComparison.OrdinalIgnoreCase)),
Permissions = permissions.ToIds()
NumClients = app.Clients.Count(x => Role.IsRole(x.Value.Role, role.Name)),
NumContributors = app.Contributors.Count(x => Role.IsRole(x.Value, role.Name)),
Permissions = permissions.ToIds(),
IsDefaultRole = Role.IsDefaultRole(role.Name)
};
return result.CreateLinks(controller, app.Name);
}
private RoleDto CreateLinks(ApiController controller, string app)
{
var values = new { app, name = Name };
if (!IsDefaultRole)
{
if (controller.HasPermission(AllPermissions.AppRolesUpdate, app) && NumClients == 0 && NumContributors == 0)
{
AddPutLink("update", controller.Url<AppRolesController>(x => nameof(x.UpdateRole), values));
}
if (controller.HasPermission(AllPermissions.AppRolesDelete, app))
{
AddDeleteLink("delete", controller.Url<AppRolesController>(x => nameof(x.DeleteRole), values));
}
}
return this;
}
}
}

31
src/Squidex/Areas/Api/Controllers/Apps/Models/RolesDto.cs

@ -8,22 +8,41 @@
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
public sealed class RolesDto
public sealed class RolesDto : Resource
{
/// <summary>
/// The app roles.
/// The roles.
/// </summary>
[Required]
public RoleDto[] Roles { get; set; }
public RoleDto[] Items { get; set; }
public static RolesDto FromApp(IAppEntity app)
public static RolesDto FromApp(IAppEntity app, ApiController controller)
{
var roles = app.Roles.Values.Select(x => RoleDto.FromRole(x, app)).ToArray();
var result = new RolesDto
{
Items = app.Roles.Values.Select(x => RoleDto.FromRole(x, app, controller)).OrderBy(x => x.Name).ToArray()
};
return result.CreateLinks(controller, app.Name);
}
private RolesDto CreateLinks(ApiController controller, string app)
{
var values = new { app };
AddSelfLink(controller.Url<AppRolesController>(x => nameof(x.GetRoles), values));
if (controller.HasPermission(Permissions.AppRolesCreate, app))
{
AddPostLink("create", controller.Url<AppRolesController>(x => nameof(x.PostRole), values));
}
return new RolesDto { Roles = roles };
return this;
}
}
}

3
src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateLanguageDto.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Reflection;
@ -26,7 +27,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// <summary>
/// Optional fallback languages.
/// </summary>
public Language[] Fallback { get; set; }
public List<Language> Fallback { get; set; }
public UpdateLanguage ToCommand(Language language)
{

34
src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs

@ -74,18 +74,18 @@ namespace Squidex.Areas.Api.Controllers.Assets
[FromQuery] int? quality = null,
[FromQuery] string mode = null)
{
IAssetEntity entity;
IAssetEntity asset;
if (Guid.TryParse(idOrSlug, out var guid))
{
entity = await assetRepository.FindAssetAsync(guid);
asset = await assetRepository.FindAssetAsync(guid);
}
else
{
entity = await assetRepository.FindAssetBySlugAsync(App.Id, idOrSlug);
asset = await assetRepository.FindAssetBySlugAsync(App.Id, idOrSlug);
}
return DeliverAsset(entity, version, width, height, quality, mode, dl);
return DeliverAsset(asset, version, width, height, quality, mode, dl);
}
/// <summary>
@ -115,25 +115,25 @@ namespace Squidex.Areas.Api.Controllers.Assets
[FromQuery] int? quality = null,
[FromQuery] string mode = null)
{
var entity = await assetRepository.FindAssetAsync(id);
var asset = await assetRepository.FindAssetAsync(id);
return DeliverAsset(entity, version, width, height, quality, mode, dl);
return DeliverAsset(asset, version, width, height, quality, mode, dl);
}
private IActionResult DeliverAsset(IAssetEntity entity, long version, int? width, int? height, int? quality, string mode, int download = 1)
private IActionResult DeliverAsset(IAssetEntity asset, long version, int? width, int? height, int? quality, string mode, int download = 1)
{
if (entity == null || entity.FileVersion < version || width == 0 || height == 0 || quality == 0)
if (asset == null || asset.FileVersion < version || width == 0 || height == 0 || quality == 0)
{
return NotFound();
}
Response.Headers[HeaderNames.ETag] = entity.FileVersion.ToString();
Response.Headers[HeaderNames.ETag] = asset.FileVersion.ToString();
var handler = new Func<Stream, Task>(async bodyStream =>
{
var assetId = entity.Id.ToString();
var assetId = asset.Id.ToString();
if (entity.IsImage && (width.HasValue || height.HasValue || quality.HasValue))
if (asset.IsImage && (width.HasValue || height.HasValue || quality.HasValue))
{
var assetSuffix = $"{width}_{height}_{mode}";
@ -144,7 +144,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
try
{
await assetStore.DownloadAsync(assetId, entity.FileVersion, assetSuffix, bodyStream);
await assetStore.DownloadAsync(assetId, asset.FileVersion, assetSuffix, bodyStream);
}
catch (AssetNotFoundException)
{
@ -156,7 +156,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
{
using (Profiler.Trace("ResizeDownload"))
{
await assetStore.DownloadAsync(assetId, entity.FileVersion, null, sourceStream);
await assetStore.DownloadAsync(assetId, asset.FileVersion, null, sourceStream);
sourceStream.Position = 0;
}
@ -168,7 +168,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
using (Profiler.Trace("ResizeUpload"))
{
await assetStore.UploadAsync(assetId, entity.FileVersion, assetSuffix, destinationStream);
await assetStore.UploadAsync(assetId, asset.FileVersion, assetSuffix, destinationStream);
destinationStream.Position = 0;
}
@ -180,17 +180,17 @@ namespace Squidex.Areas.Api.Controllers.Assets
}
else
{
await assetStore.DownloadAsync(assetId, entity.FileVersion, null, bodyStream);
await assetStore.DownloadAsync(assetId, asset.FileVersion, null, bodyStream);
}
});
if (download == 1)
{
return new FileCallbackResult(entity.MimeType, entity.FileName, true, handler);
return new FileCallbackResult(asset.MimeType, asset.FileName, true, handler);
}
else
{
return new FileCallbackResult(entity.MimeType, null, true, handler);
return new FileCallbackResult(asset.MimeType, null, true, handler);
}
}

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

@ -105,14 +105,14 @@ namespace Squidex.Areas.Api.Controllers.Assets
var assets = await assetQuery.QueryAsync(context, Q.Empty.WithODataQuery(Request.QueryString.ToString()).WithIds(ids));
var response = AssetsDto.FromAssets(assets);
var response = AssetsDto.FromAssets(assets, this, app);
if (controllerOptions.Value.EnableSurrogateKeys && response.Items.Length <= controllerOptions.Value.MaxItemsForSurrogateKeys)
{
Response.Headers["Surrogate-Key"] = response.Items.ToSurrogateKeys();
Response.Headers["Surrogate-Key"] = response.ToSurrogateKeys();
}
Response.Headers[HeaderNames.ETag] = response.Items.ToManyEtag(response.Total);
Response.Headers[HeaderNames.ETag] = response.ToEtag();
return Ok(response);
}
@ -135,21 +135,21 @@ namespace Squidex.Areas.Api.Controllers.Assets
{
var context = Context();
var entity = await assetQuery.FindAssetAsync(context, id);
var asset = await assetQuery.FindAssetAsync(context, id);
if (entity == null)
if (asset == null)
{
return NotFound();
}
var response = AssetDto.FromAsset(entity);
var response = AssetDto.FromAsset(asset, this, app);
if (controllerOptions.Value.EnableSurrogateKeys)
{
Response.Headers["Surrogate-Key"] = entity.Id.ToString();
Response.Headers["Surrogate-Key"] = asset.Id.ToString();
}
Response.Headers[HeaderNames.ETag] = entity.Version.ToString();
Response.Headers[HeaderNames.ETag] = asset.Version.ToString();
return Ok(response);
}
@ -169,8 +169,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
/// </remarks>
[HttpPost]
[Route("apps/{app}/assets/")]
[ProducesResponseType(typeof(AssetCreatedDto), 201)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ProducesResponseType(typeof(AssetDto), 200)]
[AssetRequestSizeLimit]
[ApiPermission(Permissions.AppAssetsCreate)]
[ApiCosts(1)]
@ -179,12 +178,13 @@ namespace Squidex.Areas.Api.Controllers.Assets
var assetFile = await CheckAssetFileAsync(file);
var command = new CreateAsset { File = assetFile };
var context = await CommandBus.PublishAsync(command);
var result = context.Result<AssetCreatedResult>();
var response = AssetCreatedDto.FromCommand(command, result);
var response = AssetDto.FromAsset(result.Asset, this, app, result.Tags, result.IsDuplicate);
return StatusCode(201, response);
return CreatedAtAction(nameof(GetAsset), new { app, id = response.Id }, response);
}
/// <summary>
@ -194,7 +194,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
/// <param name="id">The id of the asset.</param>
/// <param name="file">The file to upload.</param>
/// <returns>
/// 201 => Asset updated.
/// 200 => Asset updated.
/// 404 => Asset or app not found.
/// 400 => Asset exceeds the maximum size.
/// </returns>
@ -203,8 +203,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
/// </remarks>
[HttpPut]
[Route("apps/{app}/assets/{id}/content/")]
[ProducesResponseType(typeof(AssetReplacedDto), 201)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ProducesResponseType(typeof(AssetDto), 200)]
[ApiPermission(Permissions.AppAssetsUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> PutAssetContent(string app, Guid id, [SwaggerIgnore] List<IFormFile> file)
@ -212,12 +211,10 @@ namespace Squidex.Areas.Api.Controllers.Assets
var assetFile = await CheckAssetFileAsync(file);
var command = new UpdateAsset { File = assetFile, AssetId = id };
var context = await CommandBus.PublishAsync(command);
var result = context.Result<AssetSavedResult>();
var response = AssetReplacedDto.FromCommand(command, result);
var response = await InvokeCommandAsync(app, command);
return StatusCode(201, response);
return Ok(response);
}
/// <summary>
@ -227,21 +224,23 @@ namespace Squidex.Areas.Api.Controllers.Assets
/// <param name="id">The id of the asset.</param>
/// <param name="request">The asset object that needs to updated.</param>
/// <returns>
/// 204 => Asset updated.
/// 200 => Asset updated.
/// 400 => Asset name not valid.
/// 404 => Asset or app not found.
/// </returns>
[HttpPut]
[Route("apps/{app}/assets/{id}/")]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ProducesResponseType(typeof(AssetDto), 200)]
[AssetRequestSizeLimit]
[ApiPermission(Permissions.AppAssetsUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> PutAsset(string app, Guid id, [FromBody] AnnotateAssetDto request)
{
await CommandBus.PublishAsync(request.ToCommand(id));
var command = request.ToCommand(id);
return NoContent();
var response = await InvokeCommandAsync(app, command);
return Ok(response);
}
/// <summary>
@ -250,7 +249,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
/// <param name="app">The name of the app.</param>
/// <param name="id">The id of the asset to delete.</param>
/// <returns>
/// 204 => Asset has been deleted.
/// 204 => Asset deleted.
/// 404 => Asset or app not found.
/// </returns>
[HttpDelete]
@ -264,6 +263,16 @@ namespace Squidex.Areas.Api.Controllers.Assets
return NoContent();
}
private async Task<AssetDto> InvokeCommandAsync(string app, ICommand command)
{
var context = await CommandBus.PublishAsync(command);
var result = context.Result<AssetResult>();
var response = AssetDto.FromAsset(result.Asset, this, app, result.Tags);
return response;
}
private async Task<AssetFile> CheckAssetFileAsync(IReadOnlyList<IFormFile> file)
{
if (file.Count != 1)

108
src/Squidex/Areas/Api/Controllers/Assets/Models/AssetCreatedDto.cs

@ -1,108 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Infrastructure;
namespace Squidex.Areas.Api.Controllers.Assets.Models
{
public sealed class AssetCreatedDto
{
/// <summary>
/// The id of the asset.
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// The file type.
/// </summary>
[Required]
public string FileType { get; set; }
/// <summary>
/// The file name.
/// </summary>
[Required]
public string FileName { get; set; }
/// <summary>
/// The slug.
/// </summary>
[Required]
public string Slug { get; set; }
/// <summary>
/// The mime type.
/// </summary>
[Required]
public string MimeType { get; set; }
/// <summary>
/// The default tags.
/// </summary>
[Required]
public HashSet<string> Tags { get; set; }
/// <summary>
/// The size of the file in bytes.
/// </summary>
public long FileSize { get; set; }
/// <summary>
/// The version of the file.
/// </summary>
public long FileVersion { get; set; }
/// <summary>
/// Determines of the created file is an image.
/// </summary>
public bool IsImage { get; set; }
/// <summary>
/// The width of the image in pixels if the asset is an image.
/// </summary>
public int? PixelWidth { get; set; }
/// <summary>
/// The height of the image in pixels if the asset is an image.
/// </summary>
public int? PixelHeight { get; set; }
/// <summary>
/// Indicates if the asset has been already uploaded.
/// </summary>
public bool IsDuplicate { get; set; }
/// <summary>
/// The version of the asset.
/// </summary>
public long Version { get; set; }
public static AssetCreatedDto FromCommand(CreateAsset command, AssetCreatedResult result)
{
return new AssetCreatedDto
{
Id = result.IdOrValue,
FileName = command.File.FileName,
FileSize = command.File.FileSize,
FileType = command.File.FileName.FileType(),
FileVersion = result.FileVersion,
MimeType = command.File.MimeType,
IsImage = command.ImageInfo != null,
IsDuplicate = result.IsDuplicate,
PixelWidth = command.ImageInfo?.PixelWidth,
PixelHeight = command.ImageInfo?.PixelHeight,
Tags = result.Tags,
Version = result.Version
};
}
}
}

53
src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs

@ -8,15 +8,17 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Newtonsoft.Json;
using NodaTime;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Reflection;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Assets.Models
{
public sealed class AssetDto : IGenerateETag
public sealed class AssetDto : Resource, IGenerateETag
{
/// <summary>
/// The id of the asset.
@ -110,9 +112,54 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models
/// </summary>
public long Version { get; set; }
public static AssetDto FromAsset(IAssetEntity asset)
/// <summary>
/// The metadata.
/// </summary>
[JsonProperty("_meta")]
public AssetMetadata Metadata { get; set; }
public static AssetDto FromAsset(IAssetEntity asset, ApiController controller, string app, HashSet<string> tags = null, bool isDuplicate = false)
{
var response = SimpleMapper.Map(asset, new AssetDto { FileType = asset.FileName.FileType() });
if (tags != null)
{
response.Tags = tags;
}
if (isDuplicate)
{
response.Metadata = new AssetMetadata { IsDuplicate = "true" };
}
return CreateLinks(response, controller, app);
}
private static AssetDto CreateLinks(AssetDto response, ApiController controller, string app)
{
return SimpleMapper.Map(asset, new AssetDto { FileType = asset.FileName.FileType() });
var values = new { app, id = response.Id };
response.AddSelfLink(controller.Url<AssetsController>(x => nameof(x.GetAsset), values));
if (controller.HasPermission(Permissions.AppAssetsUpdate))
{
response.AddPutLink("update", controller.Url<AssetsController>(x => nameof(x.PutAsset), values));
response.AddPutLink("upload", controller.Url<AssetsController>(x => nameof(x.PutAssetContent), values));
}
if (controller.HasPermission(Permissions.AppAssetsDelete))
{
response.AddDeleteLink("delete", controller.Url<AssetsController>(x => nameof(x.DeleteAsset), values));
}
response.AddGetLink("content", controller.Url<AssetContentController>(x => nameof(x.GetAssetContent), new { id = response.Id, version = response.FileVersion }));
if (!string.IsNullOrWhiteSpace(response.Slug))
{
response.AddGetLink("content/slug", controller.Url<AssetContentController>(x => nameof(x.GetAssetContentBySlug), new { app, idOrSlug = response.Slug, version = response.Version }));
}
return response;
}
}
}

11
src/Squidex.Domain.Apps.Entities/Contents/StatusForFrontend.cs → src/Squidex/Areas/Api/Controllers/Assets/Models/AssetMetadata.cs

@ -5,12 +5,13 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Domain.Apps.Entities.Contents
namespace Squidex.Areas.Api.Controllers.Assets.Models
{
public enum StatusForFrontend
public sealed class AssetMetadata
{
PublishedDraft,
PublishedOnly,
Archived
/// <summary>
/// Indicates whether the asset is a duplicate.
/// </summary>
public string IsDuplicate { get; set; }
}
}

74
src/Squidex/Areas/Api/Controllers/Assets/Models/AssetReplacedDto.cs

@ -1,74 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.ComponentModel.DataAnnotations;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Assets.Commands;
namespace Squidex.Areas.Api.Controllers.Assets.Models
{
public sealed class AssetReplacedDto
{
/// <summary>
/// The mime type.
/// </summary>
[Required]
public string MimeType { get; set; }
/// <summary>
/// The file hash.
/// </summary>
[Required]
public string FileHash { get; set; }
/// <summary>
/// The size of the file in bytes.
/// </summary>
public long FileSize { get; set; }
/// <summary>
/// The version of the file.
/// </summary>
public long FileVersion { get; set; }
/// <summary>
/// Determines of the created file is an image.
/// </summary>
public bool IsImage { get; set; }
/// <summary>
/// The width of the image in pixels if the asset is an image.
/// </summary>
public int? PixelWidth { get; set; }
/// <summary>
/// The height of the image in pixels if the asset is an image.
/// </summary>
public int? PixelHeight { get; set; }
/// <summary>
/// The version of the asset.
/// </summary>
public long Version { get; set; }
public static AssetReplacedDto FromCommand(UpdateAsset command, AssetSavedResult result)
{
var response = new AssetReplacedDto
{
FileSize = command.File.FileSize,
FileVersion = result.FileVersion,
MimeType = command.File.MimeType,
IsImage = command.ImageInfo != null,
PixelWidth = command.ImageInfo?.PixelWidth,
PixelHeight = command.ImageInfo?.PixelHeight,
Version = result.Version
};
return response;
}
}
}

48
src/Squidex/Areas/Api/Controllers/Assets/Models/AssetsDto.cs

@ -9,25 +9,59 @@ using System.ComponentModel.DataAnnotations;
using System.Linq;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Infrastructure;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Assets.Models
{
public sealed class AssetsDto
public sealed class AssetsDto : Resource
{
/// <summary>
/// The total number of assets.
/// </summary>
public long Total { get; set; }
/// <summary>
/// The assets.
/// </summary>
[Required]
public AssetDto[] Items { get; set; }
/// <summary>
/// The total number of assets.
/// </summary>
public long Total { get; set; }
public string ToEtag()
{
return Items.ToManyEtag(Total);
}
public static AssetsDto FromAssets(IResultList<IAssetEntity> assets)
public string ToSurrogateKeys()
{
return new AssetsDto { Total = assets.Total, Items = assets.Select(AssetDto.FromAsset).ToArray() };
return Items.ToSurrogateKeys();
}
public static AssetsDto FromAssets(IResultList<IAssetEntity> assets, ApiController controller, string app)
{
var response = new AssetsDto
{
Total = assets.Total,
Items = assets.Select(x => AssetDto.FromAsset(x, controller, app)).ToArray()
};
return CreateLinks(response, controller, app);
}
private static AssetsDto CreateLinks(AssetsDto response, ApiController controller, string app)
{
var values = new { app };
response.AddSelfLink(controller.Url<AssetsController>(x => nameof(x.GetAssets), values));
if (controller.HasPermission(Permissions.AppAssetsCreate))
{
response.AddPostLink("create", controller.Url<AssetsController>(x => nameof(x.PostAsset), values));
}
response.AddGetLink("tags", controller.Url<AssetsController>(x => nameof(x.GetTags), values));
return response;
}
}
}

7
src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs

@ -12,7 +12,6 @@ using Microsoft.AspNetCore.Mvc;
using Orleans;
using Squidex.Areas.Api.Controllers.Backups.Models;
using Squidex.Domain.Apps.Entities.Backup;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Tasks;
using Squidex.Shared;
@ -44,16 +43,16 @@ namespace Squidex.Areas.Api.Controllers.Backups
/// </returns>
[HttpGet]
[Route("apps/{app}/backups/")]
[ProducesResponseType(typeof(List<BackupJobDto>), 200)]
[ProducesResponseType(typeof(BackupJobsDto), 200)]
[ApiPermission(Permissions.AppBackupsRead)]
[ApiCosts(0)]
public async Task<IActionResult> GetJobs(string app)
public async Task<IActionResult> GetBackups(string app)
{
var backupGrain = grainFactory.GetGrain<IBackupGrain>(AppId);
var jobs = await backupGrain.GetStateAsync();
var response = jobs.Value.ToArray(BackupJobDto.FromBackup);
var response = BackupJobsDto.FromBackups(jobs.Value, this, app);
return Ok(response);
}

22
src/Squidex/Areas/Api/Controllers/Backups/Models/BackupJobDto.cs

@ -9,10 +9,12 @@ using System;
using NodaTime;
using Squidex.Domain.Apps.Entities.Backup;
using Squidex.Infrastructure.Reflection;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Backups.Models
{
public sealed class BackupJobDto
public sealed class BackupJobDto : Resource
{
/// <summary>
/// The id of the backup job.
@ -44,9 +46,23 @@ namespace Squidex.Areas.Api.Controllers.Backups.Models
/// </summary>
public JobStatus Status { get; set; }
public static BackupJobDto FromBackup(IBackupJob backup)
public static BackupJobDto FromBackup(IBackupJob backup, ApiController controller, string app)
{
return SimpleMapper.Map(backup, new BackupJobDto());
var result = SimpleMapper.Map(backup, new BackupJobDto());
return result.CreateLinks(controller, app);
}
private BackupJobDto CreateLinks(ApiController controller, string app)
{
var values = new { app, id = Id };
if (controller.HasPermission(Permissions.AppBackupsDelete, app))
{
AddDeleteLink("delete", controller.Url<BackupsController>(x => nameof(x.DeleteBackup), values));
}
return this;
}
}
}

49
src/Squidex/Areas/Api/Controllers/Backups/Models/BackupJobsDto.cs

@ -0,0 +1,49 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Squidex.Domain.Apps.Entities.Backup;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Backups.Models
{
public sealed class BackupJobsDto : Resource
{
/// <summary>
/// The backups.
/// </summary>
[Required]
public BackupJobDto[] Items { get; set; }
public static BackupJobsDto FromBackups(IEnumerable<IBackupJob> backups, ApiController controller, string app)
{
var result = new BackupJobsDto
{
Items = backups.Select(x => BackupJobDto.FromBackup(x, controller, app)).ToArray()
};
return result.CreateLinks(controller, app);
}
private BackupJobsDto CreateLinks(ApiController controller, string app)
{
var values = new { app };
AddSelfLink(controller.Url<BackupsController>(x => nameof(x.GetBackups), values));
if (controller.HasPermission(Permissions.AppBackupsCreate, app))
{
AddPostLink("create", controller.Url<BackupsController>(x => nameof(x.PostBackup), values));
}
return this;
}
}
}

4
src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs

@ -41,7 +41,7 @@ namespace Squidex.Areas.Api.Controllers.Backups
[HttpGet]
[Route("apps/restore/")]
[ProducesResponseType(typeof(RestoreJobDto), 200)]
[ApiPermission(Permissions.AdminRestoreRead)]
[ApiPermission(Permissions.AdminRestore)]
public async Task<IActionResult> GetJob()
{
var restoreGrain = grainFactory.GetGrain<IRestoreGrain>(SingleGrain.Id);
@ -67,7 +67,7 @@ namespace Squidex.Areas.Api.Controllers.Backups
/// </returns>
[HttpPost]
[Route("apps/restore/")]
[ApiPermission(Permissions.AdminRestoreCreate)]
[ApiPermission(Permissions.AdminRestore)]
public async Task<IActionResult> PostRestore([FromBody] RestoreRequest request)
{
var restoreGrain = grainFactory.GetGrain<IRestoreGrain>(SingleGrain.Id);

3
src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs

@ -76,7 +76,6 @@ namespace Squidex.Areas.Api.Controllers.Comments
[HttpPost]
[Route("apps/{app}/comments/{commentsId}")]
[ProducesResponseType(typeof(EntityCreatedDto), 201)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppCommon)]
[ApiCosts(0)]
public async Task<IActionResult> PostComment(string app, Guid commentsId, [FromBody] UpsertCommentDto request)
@ -104,7 +103,6 @@ namespace Squidex.Areas.Api.Controllers.Comments
/// </returns>
[HttpPut]
[Route("apps/{app}/comments/{commentsId}/{commentId}")]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppCommon)]
[ApiCosts(0)]
public async Task<IActionResult> PutComment(string app, Guid commentsId, Guid commentId, [FromBody] UpsertCommentDto request)
@ -126,7 +124,6 @@ namespace Squidex.Areas.Api.Controllers.Comments
/// </returns>
[HttpDelete]
[Route("apps/{app}/comments/{commentsId}/{commentId}")]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppCommon)]
[ApiCosts(0)]
public async Task<IActionResult> DeleteComment(string app, Guid commentsId, Guid commentId)

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

@ -6,13 +6,10 @@
// ==========================================================================
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;
using NodaTime;
using NodaTime.Text;
using Squidex.Areas.Api.Controllers.Contents.Models;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities;
@ -21,7 +18,6 @@ using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Contents.GraphQL;
using Squidex.Infrastructure.Commands;
using Squidex.Shared;
using Squidex.Shared.Identity;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Contents
@ -111,7 +107,6 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="ids">The optional ids of the content to fetch.</param>
/// <param name="status">The requested status, only for frontend client.</param>
/// <returns>
/// 200 => Contents retrieved.
/// 404 => App not found.
@ -121,26 +116,22 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// </remarks>
[HttpGet]
[Route("content/{app}/")]
[ProducesResponseType(typeof(ContentsDto), 200)]
[ApiPermission]
[ApiCosts(1)]
public async Task<IActionResult> GetAllContents(string app, [FromQuery] string ids, [FromQuery] string status = null)
public async Task<IActionResult> GetAllContents(string app, [FromQuery] string ids)
{
var context = Context().WithFrontendStatus(status);
var result = await contentQuery.QueryAsync(context, Q.Empty.WithIds(ids).Ids);
var context = Context();
var contents = await contentQuery.QueryAsync(context, Q.Empty.WithIds(ids).Ids);
var response = new ContentsDto
{
Total = result.Count,
Items = result.Take(200).Select(x => ContentDto.FromContent(x, context)).ToArray()
};
var response = ContentsDto.FromContents(contents.Count, contents, context, this, app, null);
if (controllerOptions.Value.EnableSurrogateKeys && response.Items.Length <= controllerOptions.Value.MaxItemsForSurrogateKeys)
{
Response.Headers["Surrogate-Key"] = response.Items.ToSurrogateKeys();
Response.Headers["Surrogate-Key"] = response.ToSurrogateKeys();
}
Response.Headers[HeaderNames.ETag] = response.Items.ToManyEtag();
Response.Headers[HeaderNames.ETag] = response.ToEtag();
return Ok(response);
}
@ -151,7 +142,6 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param>
/// <param name="ids">The optional ids of the content to fetch.</param>
/// <param name="status">The requested status, only for frontend client.</param>
/// <returns>
/// 200 => Contents retrieved.
/// 404 => Schema or app not found.
@ -161,26 +151,22 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// </remarks>
[HttpGet]
[Route("content/{app}/{name}/")]
[ProducesResponseType(typeof(ContentsDto), 200)]
[ApiPermission]
[ApiCosts(1)]
public async Task<IActionResult> GetContents(string app, string name, [FromQuery] string ids = null, [FromQuery] string status = null)
public async Task<IActionResult> GetContents(string app, string name, [FromQuery] string ids = null)
{
var context = Context().WithFrontendStatus(status);
var result = await contentQuery.QueryAsync(context, name, Q.Empty.WithIds(ids).WithODataQuery(Request.QueryString.ToString()));
var context = Context();
var contents = await contentQuery.QueryAsync(context, name, Q.Empty.WithIds(ids).WithODataQuery(Request.QueryString.ToString()));
var response = new ContentsDto
{
Total = result.Total,
Items = result.Take(200).Select(x => ContentDto.FromContent(x, context)).ToArray()
};
var response = ContentsDto.FromContents(contents.Total, contents, context, this, app, name);
if (controllerOptions.Value.EnableSurrogateKeys && response.Items.Length <= controllerOptions.Value.MaxItemsForSurrogateKeys)
if (ShouldProvideSurrogateKeys(response))
{
Response.Headers["Surrogate-Key"] = response.Items.ToSurrogateKeys();
Response.Headers["Surrogate-Key"] = response.ToSurrogateKeys();
}
Response.Headers[HeaderNames.ETag] = response.Items.ToManyEtag(response.Total);
Response.Headers[HeaderNames.ETag] = response.ToEtag();
return Ok(response);
}
@ -200,6 +186,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// </remarks>
[HttpGet]
[Route("content/{app}/{name}/{id}/")]
[ProducesResponseType(typeof(ContentsDto), 200)]
[ApiPermission]
[ApiCosts(1)]
public async Task<IActionResult> GetContent(string app, string name, Guid id)
@ -207,7 +194,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
var context = Context();
var content = await contentQuery.FindContentAsync(context, name, id);
var response = ContentDto.FromContent(content, context);
var response = ContentDto.FromContent(content, context, this, app, name);
if (controllerOptions.Value.EnableSurrogateKeys)
{
@ -243,7 +230,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
var context = Context();
var content = await contentQuery.FindContentAsync(context, name, id, version);
var response = ContentDto.FromContent(content, context);
var response = ContentDto.FromContent(content, context, this, app, name);
if (controllerOptions.Value.EnableSurrogateKeys)
{
@ -272,25 +259,21 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// </remarks>
[HttpPost]
[Route("content/{app}/{name}/")]
[ProducesResponseType(typeof(ContentsDto), 200)]
[ApiPermission(Permissions.AppContentsCreate)]
[ApiCosts(1)]
public async Task<IActionResult> PostContent(string app, string name, [FromBody] NamedContentData request, [FromQuery] bool publish = false)
{
await contentQuery.ThrowIfSchemaNotExistsAsync(Context(), name);
var publishPermission = Permissions.ForApp(Permissions.AppContentsPublish, app, name);
if (publish && !User.Permissions().Includes(publishPermission))
if (publish && !this.HasPermission(Helper.StatusPermission(app, name, Status.Published)))
{
return new StatusCodeResult(123);
return new ForbidResult();
}
var command = new CreateContent { ContentId = Guid.NewGuid(), Data = request.ToCleaned(), Publish = publish };
var context = await CommandBus.PublishAsync(command);
var result = context.Result<EntityCreatedResult<NamedContentData>>();
var response = ContentDto.FromCommand(command, result);
var response = await InvokeCommandAsync(app, name, command);
return CreatedAtAction(nameof(GetContent), new { id = command.ContentId }, response);
}
@ -313,6 +296,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// </remarks>
[HttpPut]
[Route("content/{app}/{name}/{id}/")]
[ProducesResponseType(typeof(ContentsDto), 200)]
[ApiPermission(Permissions.AppContentsUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> PutContent(string app, string name, Guid id, [FromBody] NamedContentData request, [FromQuery] bool asDraft = false)
@ -320,10 +304,8 @@ namespace Squidex.Areas.Api.Controllers.Contents
await contentQuery.ThrowIfSchemaNotExistsAsync(Context(), name);
var command = new UpdateContent { ContentId = id, Data = request.ToCleaned(), AsDraft = asDraft };
var context = await CommandBus.PublishAsync(command);
var result = context.Result<ContentDataChangedResult>();
var response = result.Data;
var response = await InvokeCommandAsync(app, name, command);
return Ok(response);
}
@ -346,6 +328,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// </remarks>
[HttpPatch]
[Route("content/{app}/{name}/{id}/")]
[ProducesResponseType(typeof(ContentsDto), 200)]
[ApiPermission(Permissions.AppContentsUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> PatchContent(string app, string name, Guid id, [FromBody] NamedContentData request, [FromQuery] bool asDraft = false)
@ -353,10 +336,8 @@ namespace Squidex.Areas.Api.Controllers.Contents
await contentQuery.ThrowIfSchemaNotExistsAsync(Context(), name);
var command = new PatchContent { ContentId = id, Data = request.ToCleaned(), AsDraft = asDraft };
var context = await CommandBus.PublishAsync(command);
var result = context.Result<ContentDataChangedResult>();
var response = result.Data;
var response = await InvokeCommandAsync(app, name, command);
return Ok(response);
}
@ -367,118 +348,34 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param>
/// <param name="id">The id of the content item to publish.</param>
/// <param name="dueTime">The date and time when the content should be published.</param>
/// <param name="request">The status request.</param>
/// <returns>
/// 204 => Content published.
/// 200 => Content published.
/// 404 => Content, schema or app not found.
/// 400 => Content was already published.
/// 400 => Request is not valid.
/// </returns>
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks>
[HttpPut]
[Route("content/{app}/{name}/{id}/publish/")]
[ApiPermission(Permissions.AppContentsPublish)]
[Route("content/{app}/{name}/{id}/status/")]
[ProducesResponseType(typeof(ContentsDto), 200)]
[ApiPermission]
[ApiCosts(1)]
public async Task<IActionResult> PublishContent(string app, string name, Guid id, string dueTime = null)
public async Task<IActionResult> PutContentStatus(string app, string name, Guid id, ChangeStatusDto request)
{
await contentQuery.ThrowIfSchemaNotExistsAsync(Context(), name);
var command = CreateCommand(id, Status.Published, dueTime);
await CommandBus.PublishAsync(command);
return NoContent();
}
/// <summary>
/// Unpublish a content item.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param>
/// <param name="id">The id of the content item to unpublish.</param>
/// <param name="dueTime">The date and time when the content should be unpublished.</param>
/// <returns>
/// 204 => Content unpublished.
/// 404 => Content, schema or app not found.
/// 400 => Content was not published.
/// </returns>
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks>
[HttpPut]
[Route("content/{app}/{name}/{id}/unpublish/")]
[ApiPermission(Permissions.AppContentsUnpublish)]
[ApiCosts(1)]
public async Task<IActionResult> UnpublishContent(string app, string name, Guid id, string dueTime = null)
if (!this.HasPermission(Helper.StatusPermission(app, name, Status.Published)))
{
await contentQuery.ThrowIfSchemaNotExistsAsync(Context(), name);
var command = CreateCommand(id, Status.Draft, dueTime);
await CommandBus.PublishAsync(command);
return NoContent();
return new ForbidResult();
}
/// <summary>
/// Archive a content item.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param>
/// <param name="id">The id of the content item to archive.</param>
/// <param name="dueTime">The date and time when the content should be archived.</param>
/// <returns>
/// 204 => Content archived.
/// 404 => Content, schema or app not found.
/// 400 => Content was already archived.
/// </returns>
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks>
[HttpPut]
[Route("content/{app}/{name}/{id}/archive/")]
[ApiPermission(Permissions.AppContentsArchive)]
[ApiCosts(1)]
public async Task<IActionResult> ArchiveContent(string app, string name, Guid id, string dueTime = null)
{
await contentQuery.ThrowIfSchemaNotExistsAsync(Context(), name);
var command = CreateCommand(id, Status.Archived, dueTime);
await CommandBus.PublishAsync(command);
return NoContent();
}
/// <summary>
/// Restore a content item.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param>
/// <param name="id">The id of the content item to restore.</param>
/// <param name="dueTime">The date and time when the content should be restored.</param>
/// <returns>
/// 204 => Content restored.
/// 404 => Content, schema or app not found.
/// 400 => Content was not archived.
/// </returns>
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks>
[HttpPut]
[Route("content/{app}/{name}/{id}/restore/")]
[ApiPermission(Permissions.AppContentsRestore)]
[ApiCosts(1)]
public async Task<IActionResult> RestoreContent(string app, string name, Guid id, string dueTime = null)
{
await contentQuery.ThrowIfSchemaNotExistsAsync(Context(), name);
var command = request.ToCommand(id);
var command = CreateCommand(id, Status.Draft, dueTime);
var response = await InvokeCommandAsync(app, name, command);
await CommandBus.PublishAsync(command);
return NoContent();
return Ok(response);
}
/// <summary>
@ -488,7 +385,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// <param name="name">The name of the schema.</param>
/// <param name="id">The id of the content item to discard changes.</param>
/// <returns>
/// 204 => Content restored.
/// 200 => Content restored.
/// 404 => Content, schema or app not found.
/// 400 => Content was not archived.
/// </returns>
@ -497,17 +394,18 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// </remarks>
[HttpPut]
[Route("content/{app}/{name}/{id}/discard/")]
[ProducesResponseType(typeof(ContentsDto), 200)]
[ApiPermission(Permissions.AppContentsDiscard)]
[ApiCosts(1)]
public async Task<IActionResult> DiscardChanges(string app, string name, Guid id)
public async Task<IActionResult> DiscardDraft(string app, string name, Guid id)
{
await contentQuery.ThrowIfSchemaNotExistsAsync(Context(), name);
var command = new DiscardChanges { ContentId = id };
await CommandBus.PublishAsync(command);
var response = await InvokeCommandAsync(app, name, command);
return NoContent();
return Ok(response);
}
/// <summary>
@ -517,7 +415,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// <param name="name">The name of the schema.</param>
/// <param name="id">The id of the content item to delete.</param>
/// <returns>
/// 204 => Content has been deleted.
/// 204 => Content deleted.
/// 404 => Content, schema or app not found.
/// </returns>
/// <remarks>
@ -538,21 +436,14 @@ namespace Squidex.Areas.Api.Controllers.Contents
return NoContent();
}
private static ChangeContentStatus CreateCommand(Guid id, Status status, string dueTime)
private async Task<ContentDto> InvokeCommandAsync(string app, string schema, ICommand command)
{
Instant? dt = null;
var context = await CommandBus.PublishAsync(command);
if (!string.IsNullOrWhiteSpace(dueTime))
{
var parseResult = InstantPattern.General.Parse(dueTime);
var result = context.Result<IContentEntity>();
var response = ContentDto.FromContent(result, null, this, app, schema);
if (parseResult.Success)
{
dt = parseResult.Value;
}
}
return new ChangeContentStatus { Status = status, ContentId = id, DueTime = dt };
return response;
}
private QueryContext Context()
@ -563,5 +454,10 @@ namespace Squidex.Areas.Api.Controllers.Contents
.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;
}
}
}

88
src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaSwaggerGenerator.cs

@ -32,6 +32,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
private readonly string schemaName;
private readonly string schemaType;
private readonly string appPath;
private readonly JsonSchema4 statusSchema;
private readonly string appName;
static SchemaSwaggerGenerator()
@ -46,6 +47,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
string appPath,
Schema schema,
SchemaResolver schemaResolver,
JsonSchema4 statusSchema,
PartitionResolver partitionResolver)
{
this.document = document;
@ -53,6 +55,8 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
this.appName = appName;
this.appPath = appPath;
this.statusSchema = statusSchema;
schemaPath = schema.Name;
schemaName = schema.DisplayName();
schemaType = schema.TypeName();
@ -72,15 +76,13 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
var schemaOperations = new List<SwaggerPathItem>
{
GenerateSchemaQueryOperation(),
GenerateSchemaCreateOperation(),
GenerateSchemaGetsOperation(),
GenerateSchemaGetOperation(),
GenerateSchemaCreateOperation(),
GenerateSchemaUpdateOperation(),
GenerateSchemaPatchOperation(),
GenerateSchemaPublishOperation(),
GenerateSchemaUnpublishOperation(),
GenerateSchemaArchiveOperation(),
GenerateSchemaRestoreOperation(),
GenerateSchemaUpdatePatchOperation(),
GenerateSchemaStatusOperation(),
GenerateSchemaDiscardOperation(),
GenerateSchemaDeleteOperation()
};
@ -90,11 +92,12 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
}
}
private SwaggerPathItem GenerateSchemaQueryOperation()
private SwaggerPathItem GenerateSchemaGetsOperation()
{
return AddOperation(SwaggerOperationMethod.Get, null, $"{appPath}/{schemaPath}", operation =>
{
operation.OperationId = $"Query{schemaType}Contents";
operation.Summary = $"Queries {schemaName} contents.";
operation.Description = SchemaQueryDescription;
@ -103,7 +106,8 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
operation.AddQueryParameter("$skip", JsonObjectType.Number, "Optional number of contents to skip.");
operation.AddQueryParameter("$filter", JsonObjectType.String, "Optional OData filter.");
operation.AddQueryParameter("$search", JsonObjectType.String, "Optional OData full text search.");
operation.AddQueryParameter("orderby", JsonObjectType.String, "Optional OData order definition.");
operation.AddQueryParameter("$orderby", JsonObjectType.String, "Optional OData order definition.");
operation.AddQueryParameter("$orderby", JsonObjectType.String, "Optional OData order definition.");
operation.AddResponse("200", $"{schemaName} content retrieved.", CreateContentsSchema(schemaName, contentSchema));
@ -116,6 +120,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
return AddOperation(SwaggerOperationMethod.Get, schemaName, $"{appPath}/{schemaPath}/{{id}}", operation =>
{
operation.OperationId = $"Get{schemaType}Content";
operation.Summary = $"Get a {schemaName} content.";
operation.AddResponse("200", $"{schemaName} content found.", contentSchema);
@ -129,12 +134,14 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
return AddOperation(SwaggerOperationMethod.Post, null, $"{appPath}/{schemaPath}", operation =>
{
operation.OperationId = $"Create{schemaType}Content";
operation.Summary = $"Create a {schemaName} content.";
operation.AddBodyParameter("data", dataSchema, SchemaBodyDescription);
operation.AddQueryParameter("publish", JsonObjectType.Boolean, "Set to true to autopublish content.");
operation.AddResponse("201", $"{schemaName} content created.", contentSchema);
operation.AddResponse("200", $"{schemaName} content created.", contentSchema);
operation.AddResponse("400", "Content data valid.");
AddSecurity(operation, Permissions.AppContentsCreate);
});
@ -145,80 +152,64 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
return AddOperation(SwaggerOperationMethod.Put, schemaName, $"{appPath}/{schemaPath}/{{id}}", operation =>
{
operation.OperationId = $"Update{schemaType}Content";
operation.Summary = $"Update a {schemaName} content.";
operation.AddBodyParameter("data", dataSchema, SchemaBodyDescription);
operation.AddResponse("200", $"{schemaName} content updated.", dataSchema);
operation.AddResponse("200", $"{schemaName} content updated.", contentSchema);
operation.AddResponse("400", "Content data valid.");
AddSecurity(operation, Permissions.AppContentsUpdate);
});
}
private SwaggerPathItem GenerateSchemaPatchOperation()
private SwaggerPathItem GenerateSchemaUpdatePatchOperation()
{
return AddOperation(SwaggerOperationMethod.Patch, schemaName, $"{appPath}/{schemaPath}/{{id}}", operation =>
{
operation.OperationId = $"Path{schemaType}Content";
operation.Summary = $"Patch a {schemaName} content.";
operation.AddBodyParameter("data", dataSchema, SchemaBodyDescription);
operation.AddResponse("200", $"{schemaName} content patched.", dataSchema);
operation.AddResponse("200", $"{schemaName} content patched.", contentSchema);
operation.AddResponse("400", "Status change not valid.");
AddSecurity(operation, Permissions.AppContentsUpdate);
});
}
private SwaggerPathItem GenerateSchemaPublishOperation()
private SwaggerPathItem GenerateSchemaStatusOperation()
{
return AddOperation(SwaggerOperationMethod.Put, schemaName, $"{appPath}/{schemaPath}/{{id}}/publish", operation =>
return AddOperation(SwaggerOperationMethod.Put, schemaName, $"{appPath}/{schemaPath}/{{id}}/status", operation =>
{
operation.OperationId = $"Publish{schemaType}Content";
operation.Summary = $"Publish a {schemaName} content.";
operation.OperationId = $"Change{schemaType}ContentStatus";
operation.AddResponse("204", $"{schemaName} content published.");
operation.Summary = $"Change status of {schemaName} content.";
AddSecurity(operation, Permissions.AppContentsPublish);
});
}
operation.AddBodyParameter("request", statusSchema, "The request to change content status.");
private SwaggerPathItem GenerateSchemaUnpublishOperation()
{
return AddOperation(SwaggerOperationMethod.Put, schemaName, $"{appPath}/{schemaPath}/{{id}}/unpublish", operation =>
{
operation.OperationId = $"Unpublish{schemaType}Content";
operation.Summary = $"Unpublish a {schemaName} content.";
operation.AddResponse("204", $"{schemaName} content unpublished.");
operation.AddResponse("204", $"{schemaName} content status changed.", contentSchema);
operation.AddResponse("400", "Content data valid.");
AddSecurity(operation, Permissions.AppContentsUnpublish);
AddSecurity(operation, Permissions.AppContentsStatus);
});
}
private SwaggerPathItem GenerateSchemaArchiveOperation()
private SwaggerPathItem GenerateSchemaDiscardOperation()
{
return AddOperation(SwaggerOperationMethod.Put, schemaName, $"{appPath}/{schemaPath}/{{id}}/archive", operation =>
return AddOperation(SwaggerOperationMethod.Put, schemaName, $"{appPath}/{schemaPath}/{{id}}/discard", operation =>
{
operation.OperationId = $"Archive{schemaType}Content";
operation.Summary = $"Archive a {schemaName} content.";
operation.AddResponse("204", $"{schemaName} content restored.");
AddSecurity(operation, Permissions.AppContentsRead);
});
}
operation.OperationId = $"Discard{schemaType}Content";
private SwaggerPathItem GenerateSchemaRestoreOperation()
{
return AddOperation(SwaggerOperationMethod.Put, schemaName, $"{appPath}/{schemaPath}/{{id}}/restore", operation =>
{
operation.OperationId = $"Restore{schemaType}Content";
operation.Summary = $"Restore a {schemaName} content.";
operation.Summary = $"Discard changes of {schemaName} content.";
operation.AddResponse("204", $"{schemaName} content restored.");
operation.AddResponse("400", "No pending draft.");
operation.AddResponse("200", $"{schemaName} content status changed.", contentSchema);
AddSecurity(operation, Permissions.AppContentsRestore);
AddSecurity(operation, Permissions.AppContentsDiscard);
});
}
@ -227,6 +218,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
return AddOperation(SwaggerOperationMethod.Delete, schemaName, $"{appPath}/{schemaPath}/{{id}}/", operation =>
{
operation.OperationId = $"Delete{schemaType}Content";
operation.Summary = $"Delete a {schemaName} content.";
operation.AddResponse("204", $"{schemaName} content deleted.");

28
src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasSwaggerGenerator.cs

@ -18,6 +18,7 @@ using NSwag.SwaggerGeneration;
using NSwag.SwaggerGeneration.Processors;
using NSwag.SwaggerGeneration.Processors.Contexts;
using Squidex.Areas.Api.Config.Swagger;
using Squidex.Areas.Api.Controllers.Contents.Models;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
@ -32,6 +33,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
private readonly SwaggerDocumentSettings settings = new SwaggerDocumentSettings();
private SwaggerJsonSchemaGenerator schemaGenerator;
private SwaggerDocument document;
private JsonSchema4 statusSchema;
private JsonSchemaResolver schemaResolver;
public SchemasSwaggerGenerator(IOptions<UrlsOptions> urlOptions, IEnumerable<IDocumentProcessor> documentProcessors)
@ -53,9 +55,9 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
schemaGenerator = new SwaggerJsonSchemaGenerator(settings);
schemaResolver = new SwaggerSchemaResolver(document, settings);
GenerateSchemasOperations(schemas, app);
statusSchema = await GenerateStatusSchemaAsync();
await GenerateDefaultErrorsAsync();
GenerateSchemasOperations(schemas, app);
var context =
new DocumentProcessorContext(document,
@ -73,25 +75,23 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
return document;
}
private void GenerateSchemasOperations(IEnumerable<ISchemaEntity> schemas, IAppEntity app)
private Task<JsonSchema4> GenerateStatusSchemaAsync()
{
var appBasePath = $"/content/{app.Name}";
var errorType = typeof(ChangeStatusDto);
foreach (var schema in schemas.Select(x => x.SchemaDef).Where(x => x.IsPublished))
{
new SchemaSwaggerGenerator(document, app.Name, appBasePath, schema, AppendSchema, app.PartitionResolver()).GenerateSchemaOperations();
}
return schemaGenerator.GenerateWithReferenceAsync<JsonSchema4>(errorType, Enumerable.Empty<Attribute>(), schemaResolver);
}
private async Task GenerateDefaultErrorsAsync()
private void GenerateSchemasOperations(IEnumerable<ISchemaEntity> schemas, IAppEntity app)
{
const string errorDescription = "Operation failed with internal server error.";
var errorDtoSchema = await schemaGenerator.GetErrorDtoSchemaAsync(schemaResolver);
var appBasePath = $"/content/{app.Name}";
foreach (var operation in document.Paths.Values.SelectMany(x => x.Values))
foreach (var schema in schemas.Select(x => x.SchemaDef).Where(x => x.IsPublished))
{
operation.Responses.Add("500", new SwaggerResponse { Description = errorDescription, Schema = errorDtoSchema });
var partition = app.PartitionResolver();
new SchemaSwaggerGenerator(document, app.Name, appBasePath, schema, AppendSchema, statusSchema, partition)
.GenerateSchemaOperations();
}
}

23
src/Squidex/Areas/Api/Controllers/Contents/Helper.cs

@ -0,0 +1,23 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure.Security;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Contents
{
public static class Helper
{
public static Permission StatusPermission(string app, string schema, Status status)
{
var id = Permissions.AppContentsStatus.Replace("{status}", status.ToString());
return Permissions.ForApp(id, app, schema);
}
}
}

20
src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorAssignedDto.cs → src/Squidex/Areas/Api/Controllers/Contents/Models/ChangeStatusDto.cs

@ -5,26 +5,30 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.ComponentModel.DataAnnotations;
using NodaTime;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Contents.Commands;
namespace Squidex.Areas.Api.Controllers.Apps.Models
namespace Squidex.Areas.Api.Controllers.Contents.Models
{
public sealed class ContributorAssignedDto
public sealed class ChangeStatusDto
{
/// <summary>
/// The id of the user that has been assigned as contributor.
/// The new status.
/// </summary>
[Required]
public string ContributorId { get; set; }
public Status Status { get; set; }
/// <summary>
/// Indicates if the user was created.
/// The due time.
/// </summary>
public bool IsCreated { get; set; }
public Instant? DueTime { get; set; }
public static ContributorAssignedDto FromId(string id, bool isCreated)
public ChangeContentStatus ToCommand(Guid id)
{
return new ContributorAssignedDto { ContributorId = id, IsCreated = isCreated };
return new ChangeContentStatus { ContentId = id, Status = Status, DueTime = DueTime };
}
}
}

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

@ -12,15 +12,14 @@ using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ConvertContent;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Reflection;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Contents.Models
{
public sealed class ContentDto : IGenerateETag
public sealed class ContentDto : Resource, IGenerateETag
{
/// <summary>
/// The if of the content item.
@ -80,30 +79,11 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
/// </summary>
public long Version { get; set; }
public static ContentDto FromCommand(CreateContent command, EntityCreatedResult<NamedContentData> result)
{
var now = SystemClock.Instance.GetCurrentInstant();
var response = new ContentDto
{
Id = command.ContentId,
Data = result.IdOrValue,
Version = result.Version,
Created = now,
CreatedBy = command.Actor,
LastModified = now,
LastModifiedBy = command.Actor,
Status = command.Publish ? Status.Published : Status.Draft
};
return response;
}
public static ContentDto FromContent(IContentEntity content, QueryContext context)
public static ContentDto FromContent(IContentEntity content, QueryContext context, ApiController controller, string app, string schema)
{
var response = SimpleMapper.Map(content, new ContentDto());
if (context.Flatten)
if (context?.Flatten == true)
{
response.Data = content.Data?.ToFlatten();
response.DataDraft = content.DataDraft?.ToFlatten();
@ -119,7 +99,61 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
response.ScheduleJob = SimpleMapper.Map(content.ScheduleJob, new ScheduleJobDto());
}
return response;
return response.CreateLinks(controller, app, schema);
}
private ContentDto CreateLinks(ApiController controller, string app, string schema)
{
var values = new { app, name = schema, id = Id };
AddSelfLink(controller.Url<ContentsController>(x => nameof(x.GetContent), values));
if (Version > 0)
{
var versioned = new { app, name = schema, id = Id, version = Version - 1 };
AddGetLink("prev", controller.Url<ContentsController>(x => nameof(x.GetContentVersion), versioned));
}
if (IsPending)
{
if (controller.HasPermission(Permissions.AppContentsDiscard, app, schema))
{
AddPutLink("draft/discard", controller.Url<ContentsController>(x => nameof(x.DiscardDraft), values));
}
if (controller.HasPermission(Helper.StatusPermission(app, schema, Status.Published)))
{
AddPutLink("draft/publish", controller.Url<ContentsController>(x => nameof(x.PutContentStatus), values));
}
}
if (controller.HasPermission(Permissions.AppContentsUpdate, app, schema))
{
AddPutLink("update", controller.Url<ContentsController>(x => nameof(x.PutContent), values));
if (Status == Status.Published)
{
AddPutLink("draft/propose", controller.Url<ContentsController>(x => nameof(x.PutContent), values) + "?asDraft=true");
}
AddPatchLink("patch", controller.Url<ContentsController>(x => nameof(x.PatchContent), values));
}
if (controller.HasPermission(Permissions.AppContentsDelete, app, schema))
{
AddDeleteLink("delete", controller.Url<ContentsController>(x => nameof(x.DeleteContent), values));
}
foreach (var next in StatusFlow.Next(Status))
{
if (controller.HasPermission(Helper.StatusPermission(app, schema, next)))
{
AddPutLink($"status/{next}", controller.Url<ContentsController>(x => nameof(x.PutContentStatus), values));
}
}
return this;
}
}
}

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

@ -5,9 +5,19 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Infrastructure;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Contents.Models
{
public sealed class ContentsDto
public sealed class ContentsDto : Resource
{
/// <summary>
/// The total number of content items.
@ -17,6 +27,61 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
/// <summary>
/// The content items.
/// </summary>
[Required]
public ContentDto[] Items { get; set; }
/// <summary>
/// The possible statuses.
/// </summary>
[Required]
public string[] Statuses { get; set; }
public string ToEtag()
{
return Items.ToManyEtag(Total);
}
public string ToSurrogateKeys()
{
return Items.ToSurrogateKeys();
}
public static ContentsDto FromContents(long total, IEnumerable<IContentEntity> contents, QueryContext context,
ApiController controller,
string app,
string schema)
{
var result = new ContentsDto
{
Total = total,
Items = contents.Select(x => ContentDto.FromContent(x, context, controller, app, schema)).ToArray(),
};
result.Statuses = new string[] { "Archived", "Draft", "Published" };
return result.CreateLinks(controller, app, schema);
}
private ContentsDto CreateLinks(ApiController controller, string app, string schema)
{
if (schema != null)
{
var values = new { app, name = schema };
AddSelfLink(controller.Url<ContentsController>(x => nameof(x.GetContents), values));
if (controller.HasPermission(Permissions.AppContentsCreate, app, schema))
{
AddPostLink("create", controller.Url<ContentsController>(x => nameof(x.PostContent), values));
if (controller.HasPermission(Helper.StatusPermission(app, schema, Status.Published)))
{
AddPostLink("create/publish", controller.Url<ContentsController>(x => nameof(x.PostContent), values) + "?publish=true");
}
}
}
return this;
}
}
}

33
src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Orleans;
@ -30,44 +29,54 @@ namespace Squidex.Areas.Api.Controllers.EventConsumers
[HttpGet]
[Route("event-consumers/")]
[ProducesResponseType(typeof(EventConsumersDto), 200)]
[ApiPermission(Permissions.AdminEventsRead)]
public async Task<IActionResult> GetEventConsumers()
{
var entities = await GetGrain().GetConsumersAsync();
var eventConsumers = await GetGrain().GetConsumersAsync();
var response = entities.Value.OrderBy(x => x.Name).Select(EventConsumerDto.FromEventConsumerInfo).ToArray();
var response = EventConsumersDto.FromResults(eventConsumers.Value, this);
return Ok(response);
}
[HttpPut]
[Route("event-consumers/{name}/start/")]
[ProducesResponseType(typeof(EventConsumerDto), 200)]
[ApiPermission(Permissions.AdminEventsManage)]
public async Task<IActionResult> Start(string name)
public async Task<IActionResult> StartEventConsumer(string name)
{
await GetGrain().StartAsync(name);
var eventConsumer = await GetGrain().StartAsync(name);
return NoContent();
var response = EventConsumerDto.FromEventConsumerInfo(eventConsumer.Value, this);
return Ok(response);
}
[HttpPut]
[Route("event-consumers/{name}/stop/")]
[ProducesResponseType(typeof(EventConsumerDto), 200)]
[ApiPermission(Permissions.AdminEventsManage)]
public async Task<IActionResult> Stop(string name)
public async Task<IActionResult> StopEventConsumer(string name)
{
await GetGrain().StopAsync(name);
var eventConsumer = await GetGrain().StopAsync(name);
var response = EventConsumerDto.FromEventConsumerInfo(eventConsumer.Value, this);
return NoContent();
return Ok(response);
}
[HttpPut]
[Route("event-consumers/{name}/reset/")]
[ProducesResponseType(typeof(EventConsumerDto), 200)]
[ApiPermission(Permissions.AdminEventsManage)]
public async Task<IActionResult> Reset(string name)
public async Task<IActionResult> ResetEventConsumer(string name)
{
await GetGrain().ResetAsync(name);
var eventConsumer = await GetGrain().ResetAsync(name);
var response = EventConsumerDto.FromEventConsumerInfo(eventConsumer.Value, this);
return NoContent();
return Ok(response);
}
private IEventConsumerManagerGrain GetGrain()

34
src/Squidex/Areas/Api/Controllers/EventConsumers/Models/EventConsumerDto.cs

@ -7,10 +7,12 @@
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Reflection;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.EventConsumers.Models
{
public sealed class EventConsumerDto
public sealed class EventConsumerDto : Resource
{
public bool IsStopped { get; set; }
@ -22,9 +24,35 @@ namespace Squidex.Areas.Api.Controllers.EventConsumers.Models
public string Position { get; set; }
public static EventConsumerDto FromEventConsumerInfo(EventConsumerInfo eventConsumerInfo)
public static EventConsumerDto FromEventConsumerInfo(EventConsumerInfo eventConsumerInfo, ApiController controller)
{
return SimpleMapper.Map(eventConsumerInfo, new EventConsumerDto());
var result = SimpleMapper.Map(eventConsumerInfo, new EventConsumerDto());
return result.CreateLinks(controller);
}
private EventConsumerDto CreateLinks(ApiController controller)
{
if (controller.HasPermission(Permissions.AdminEventsManage))
{
var values = new { name = Name };
if (!IsResetting)
{
AddPutLink("reset", controller.Url<EventConsumersController>(x => nameof(x.ResetEventConsumer), values));
}
if (IsStopped)
{
AddPutLink("start", controller.Url<EventConsumersController>(x => nameof(x.StartEventConsumer), values));
}
else
{
AddPutLink("stop", controller.Url<EventConsumersController>(x => nameof(x.StopEventConsumer), values));
}
}
return this;
}
}
}

39
src/Squidex/Areas/Api/Controllers/EventConsumers/Models/EventConsumersDto.cs

@ -0,0 +1,39 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.EventConsumers.Models
{
public sealed class EventConsumersDto : Resource
{
/// <summary>
/// The event consumers.
/// </summary>
public EventConsumerDto[] Items { get; set; }
public static EventConsumersDto FromResults(IEnumerable<EventConsumerInfo> items, ApiController controller)
{
var result = new EventConsumersDto
{
Items = items.Select(x => EventConsumerDto.FromEventConsumerInfo(x, controller)).ToArray()
};
return result.CreateLinks(controller);
}
private EventConsumersDto CreateLinks(ApiController controller)
{
AddSelfLink(controller.Url<EventConsumersController>(c => nameof(c.GetEventConsumers)));
return this;
}
}
}

4
src/Squidex/Areas/Api/Controllers/History/HistoryController.cs

@ -46,9 +46,9 @@ namespace Squidex.Areas.Api.Controllers.History
[ApiCosts(0.1)]
public async Task<IActionResult> GetHistory(string app, string channel)
{
var entities = await historyService.QueryByChannelAsync(AppId, channel, 100);
var events = await historyService.QueryByChannelAsync(AppId, channel, 100);
var response = entities.ToArray(HistoryEventDto.FromHistoryEvent);
var response = events.ToArray(HistoryEventDto.FromHistoryEvent);
return Ok(response);
}

4
src/Squidex/Areas/Api/Controllers/News/Service/FeaturesService.cs

@ -43,9 +43,9 @@ namespace Squidex.Areas.Api.Controllers.News.Service
if (client != null && version < FeatureVersion)
{
var entities = await client.GetAsync(filter: $"data/version/iv gt {version}", context: Flatten);
var features = await client.GetAsync(filter: $"data/version/iv gt {version}", context: Flatten);
result.Features.AddRange(entities.Items.Select(x => x.Data));
result.Features.AddRange(features.Items.Select(x => x.Data));
}
return result;

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

@ -71,10 +71,9 @@ namespace Squidex.Areas.Api.Controllers.Plans
[HttpPut]
[Route("apps/{app}/plan/")]
[ProducesResponseType(typeof(PlanChangedDto), 200)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppPlansChange)]
[ApiCosts(0)]
public async Task<IActionResult> ChangePlanAsync(string app, [FromBody] ChangePlanDto request)
public async Task<IActionResult> PutPlan(string app, [FromBody] ChangePlanDto request)
{
var context = await CommandBus.PublishAsync(request.ToCommand());

44
src/Squidex/Areas/Api/Controllers/Rules/Models/RuleDto.cs

@ -14,11 +14,12 @@ using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Entities.Rules;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Reflection;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Rules.Models
{
public sealed class RuleDto : IGenerateETag
public sealed class RuleDto : Resource, IGenerateETag
{
/// <summary>
/// The id of the rule.
@ -70,19 +71,48 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models
[JsonConverter(typeof(RuleActionConverter))]
public RuleAction Action { get; set; }
public static RuleDto FromRule(IRuleEntity rule)
public static RuleDto FromRule(IRuleEntity rule, ApiController controller, string app)
{
var response = new RuleDto();
var result = new RuleDto();
SimpleMapper.Map(rule, response);
SimpleMapper.Map(rule.RuleDef, response);
SimpleMapper.Map(rule, result);
SimpleMapper.Map(rule.RuleDef, result);
if (rule.RuleDef.Trigger != null)
{
response.Trigger = RuleTriggerDtoFactory.Create(rule.RuleDef.Trigger);
result.Trigger = RuleTriggerDtoFactory.Create(rule.RuleDef.Trigger);
}
return response;
return result.CreateLinks(controller, app);
}
private RuleDto CreateLinks(ApiController controller, string app)
{
var values = new { app, id = Id };
if (controller.HasPermission(Permissions.AppRulesDisable, app))
{
if (IsEnabled)
{
AddPutLink("disable", controller.Url<RulesController>(x => nameof(x.DisableRule), values));
}
else
{
AddPutLink("enable", controller.Url<RulesController>(x => nameof(x.EnableRule), values));
}
}
if (controller.HasPermission(Permissions.AppRulesUpdate))
{
AddPutLink("update", controller.Url<RulesController>(x => nameof(x.PutRule), values));
}
if (controller.HasPermission(Permissions.AppRulesDelete))
{
AddDeleteLink("delete", controller.Url<RulesController>(x => nameof(x.DeleteRule), values));
}
return this;
}
}
}

27
src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventDto.cs

@ -11,10 +11,11 @@ using NodaTime;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Entities.Rules;
using Squidex.Infrastructure.Reflection;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Rules.Models
{
public sealed class RuleEventDto
public sealed class RuleEventDto : Resource
{
/// <summary>
/// The id of the event.
@ -63,14 +64,28 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models
/// </summary>
public RuleJobResult JobResult { get; set; }
public static RuleEventDto FromRuleEvent(IRuleEventEntity ruleEvent)
public static RuleEventDto FromRuleEvent(IRuleEventEntity ruleEvent, ApiController controller, string app)
{
var response = new RuleEventDto();
var result = new RuleEventDto();
SimpleMapper.Map(ruleEvent, response);
SimpleMapper.Map(ruleEvent.Job, response);
SimpleMapper.Map(ruleEvent, result);
SimpleMapper.Map(ruleEvent.Job, result);
return response;
return result.CreateLinks(controller, app);
}
private RuleEventDto CreateLinks(ApiController controller, string app)
{
var values = new { app, id = Id };
AddPutLink("update", controller.Url<RulesController>(x => nameof(x.PutEvent), values));
if (NextAttempt.HasValue)
{
AddDeleteLink("delete", controller.Url<RulesController>(x => nameof(x.DeleteEvent), values));
}
return this;
}
}
}

20
src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventsDto.cs

@ -9,10 +9,11 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Squidex.Domain.Apps.Entities.Rules;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Rules.Models
{
public sealed class RuleEventsDto
public sealed class RuleEventsDto : Resource
{
/// <summary>
/// The rule events.
@ -25,9 +26,22 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models
/// </summary>
public long Total { get; set; }
public static RuleEventsDto FromRuleEvents(IReadOnlyList<IRuleEventEntity> items, long total)
public static RuleEventsDto FromRuleEvents(IReadOnlyList<IRuleEventEntity> items, long total, ApiController controller, string app)
{
return new RuleEventsDto { Total = total, Items = items.Select(RuleEventDto.FromRuleEvent).ToArray() };
var result = new RuleEventsDto
{
Total = total,
Items = items.Select(x => RuleEventDto.FromRuleEvent(x, controller, app)).ToArray()
};
return result.CreateLinks(controller, app);
}
private RuleEventsDto CreateLinks(ApiController controller, string app)
{
AddSelfLink(controller.Url<RulesController>(x => nameof(x.GetEvents), new { app }));
return this;
}
}
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save