Browse Source

Merge branch 'master' into refactoring/sc

# Conflicts:
#	src/Squidex/app/features/content/shared/assets-editor.component.html
#	src/Squidex/app/features/content/shared/assets-editor.component.ts
#	src/Squidex/app/shared/components/assets-selector.component.ts
#	src/Squidex/app/shared/components/markdown-editor.component.html
#	src/Squidex/app/shared/components/markdown-editor.component.ts
#	src/Squidex/app/shared/components/permission.directive.ts
#	src/Squidex/app/shared/components/rich-editor.component.ts
pull/345/head
Sebastian Stehle 7 years ago
parent
commit
abdd7432e5
  1. 4
      src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayField.cs
  2. 2
      src/Squidex.Domain.Apps.Core.Model/Schemas/FieldCollection.cs
  3. 4
      src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs
  4. 2
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs
  5. 8
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs
  6. 2
      src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs
  7. 2
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs
  8. 2
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidatorsFactory.cs
  9. 2
      src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs
  10. 2
      src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexGrain.cs
  11. 2
      src/Squidex.Domain.Apps.Entities/EntityMapper.cs
  12. 4
      src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs
  13. 4
      src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs
  14. 2
      src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs
  15. 4
      src/Squidex.Infrastructure/EventSourcing/RetrySubscription.cs
  16. 4
      src/Squidex.Infrastructure/Log/LockingLogStore.cs
  17. 5
      src/Squidex.Infrastructure/Security/Extensions.cs
  18. 2
      src/Squidex.Infrastructure/States/IStore.cs
  19. 11
      src/Squidex.Shared/Identity/ClaimsPrincipalExtensions.cs
  20. 4
      src/Squidex.Shared/Identity/SquidexClaimTypes.cs
  21. 6
      src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs
  22. 2
      src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs
  23. 2
      src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs
  24. 2
      src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs
  25. 13
      src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs
  26. 1
      src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaSwaggerGenerator.cs
  27. 2
      src/Squidex/Areas/Api/Controllers/Ping/PingController.cs
  28. 2
      src/Squidex/Areas/Api/Controllers/Users/UsersController.cs
  29. 40
      src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs
  30. 9
      src/Squidex/Config/Logging.cs
  31. 9
      src/Squidex/Config/MyIdentityOptions.cs
  32. 11
      src/Squidex/Pipeline/Extensions.cs
  33. 9
      src/Squidex/Pipeline/Squid/SquidMiddleware.cs
  34. 2
      src/Squidex/app/features/assets/pages/assets-page.component.ts
  35. 5
      src/Squidex/app/features/content/shared/assets-editor.component.html
  36. 22
      src/Squidex/app/features/content/shared/assets-editor.component.ts
  37. 2
      src/Squidex/app/features/content/shared/references-editor.component.ts
  38. 2
      src/Squidex/app/features/rules/pages/rules/triggers/asset-changed-trigger.component.html
  39. 2
      src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.html
  40. 56
      src/Squidex/app/framework/angular/forms/file-drop.directive.ts
  41. 4
      src/Squidex/app/shared/components/assets-list.component.html
  42. 22
      src/Squidex/app/shared/components/assets-list.component.ts
  43. 4
      src/Squidex/app/shared/components/assets-selector.component.ts
  44. 6
      src/Squidex/app/shared/components/markdown-editor.component.html
  45. 47
      src/Squidex/app/shared/components/markdown-editor.component.ts
  46. 6
      src/Squidex/app/shared/components/rich-editor.component.html
  47. 84
      src/Squidex/app/shared/components/rich-editor.component.ts
  48. 1
      src/Squidex/app/shared/state/contents.forms.ts
  49. 10
      src/Squidex/appsettings.json
  50. 2
      tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/AssertHelper.cs
  51. 1
      tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs
  52. 2
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentGrainTests.cs

4
src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayField.cs

@ -37,12 +37,12 @@ namespace Squidex.Domain.Apps.Core.Schemas
}
public ArrayField(long id, string name, Partitioning partitioning, ArrayFieldProperties properties = null, IFieldSettings settings = null)
: base(id, name, partitioning, properties)
: base(id, name, partitioning, properties, settings)
{
}
public ArrayField(long id, string name, Partitioning partitioning, NestedField[] fields, ArrayFieldProperties properties = null, IFieldSettings settings = null)
: this(id, name, partitioning, properties)
: this(id, name, partitioning, properties, settings)
{
Guard.NotNull(fields, nameof(fields));

2
src/Squidex.Domain.Apps.Core.Model/Schemas/FieldCollection.cs

@ -61,7 +61,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
}
else
{
fieldsByName = fieldsOrdered.ToDictionary(x => x.Name, StringComparer.OrdinalIgnoreCase);
fieldsByName = fieldsOrdered.ToDictionary(x => x.Name);
}
}

4
src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs

@ -95,7 +95,7 @@ namespace Squidex.Domain.Apps.Core.EventSynchronization
{
foreach (var sourceField in source.Ordered)
{
if (!target.ByName.TryGetValue(sourceField.Name, out var targetField))
if (!target.ByName.TryGetValue(sourceField.Name, out _))
{
var id = sourceField.NamedId();
@ -184,7 +184,7 @@ namespace Squidex.Domain.Apps.Core.EventSynchronization
if ((sourceField == null || sourceField is IArrayField) && targetField is IArrayField targetArrayField)
{
var fields = (sourceField as IArrayField)?.FieldCollection ?? FieldCollection<NestedField>.Empty;
var fields = ((IArrayField)sourceField)?.FieldCollection ?? FieldCollection<NestedField>.Empty;
var events = SyncFields(fields, targetArrayField.FieldCollection, serializer, idGenerator, id, options);

2
src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs

@ -65,7 +65,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules
return null;
}
if (!(@event.Payload is AppEvent appEvent))
if (!(@event.Payload is AppEvent))
{
return null;
}

8
src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs

@ -33,9 +33,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules
bool IRuleTriggerHandler.Trigger(EnrichedEvent @event, RuleTrigger trigger)
{
var typed = @event as TEnrichedEvent;
if (typed != null)
if (@event is TEnrichedEvent typed)
{
return Trigger(typed, (TTrigger)trigger);
}
@ -45,9 +43,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules
bool IRuleTriggerHandler.Trigger(AppEvent @event, RuleTrigger trigger, Guid ruleId)
{
var typed = @event as TEvent;
if (typed != null)
if (@event is TEvent typed)
{
return Trigger(typed, (TTrigger)trigger, ruleId);
}

2
src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs

@ -24,7 +24,7 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
switch (value)
{
case JsonNull n:
case JsonNull _:
return JsValue.Null;
case JsonScalar<string> s:
return new JsString(s.Value);

2
src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs

@ -69,7 +69,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
private IValidator CreateSchemaValidator(bool isPartial)
{
var fieldsValidators = new Dictionary<string, (bool IsOptional, IValidator Validator)>();
var fieldsValidators = new Dictionary<string, (bool IsOptional, IValidator Validator)>(schema.Fields.Count);
foreach (var field in schema.FieldsByName)
{

2
src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidatorsFactory.cs

@ -38,7 +38,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
yield return new CollectionValidator(field.Properties.IsRequired, field.Properties.MinItems, field.Properties.MaxItems);
}
var nestedSchema = new Dictionary<string, (bool IsOptional, IValidator Validator)>();
var nestedSchema = new Dictionary<string, (bool IsOptional, IValidator Validator)>(field.Fields.Count);
foreach (var nestedField in field.Fields)
{

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

@ -29,8 +29,8 @@ namespace Squidex.Domain.Apps.Entities.Apps
private readonly IUserResolver userResolver;
private readonly IAppsByNameIndex appsByNameIndex;
private readonly HashSet<string> contributors = new HashSet<string>();
private readonly Dictionary<string, RefToken> userMapping = new Dictionary<string, RefToken>();
private Dictionary<string, string> usersWithEmail = new Dictionary<string, string>();
private Dictionary<string, RefToken> userMapping = new Dictionary<string, RefToken>();
private bool isReserved;
private string appName;

2
src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexGrain.cs

@ -23,7 +23,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes
[CollectionName("Index_AppsByName")]
public sealed class GrainState
{
public Dictionary<string, Guid> Apps { get; set; } = new Dictionary<string, Guid>();
public Dictionary<string, Guid> Apps { get; set; } = new Dictionary<string, Guid>(StringComparer.Ordinal);
}
public AppsByNameIndexGrain(IStore<string> store)

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

@ -46,7 +46,7 @@ namespace Squidex.Domain.Apps.Entities
private static void SetCreated(IEntity entity, EnvelopeHeaders headers)
{
if (entity is IUpdateableEntity updateable && updateable.Created == default(Instant))
if (entity is IUpdateableEntity updateable && updateable.Created == default)
{
updateable.Created = headers.Timestamp();
}

4
src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs

@ -79,7 +79,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking
{
var target = kvp.Value;
var (from, to) = GetDateRange(today, target.NumDays);
var (from, _) = GetDateRange(today, target.NumDays);
if (!target.Triggered.HasValue || target.Triggered < from)
{
@ -107,7 +107,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking
await WriteStateAsync();
}
private (DateTime, DateTime) GetDateRange(DateTime today, int? numDays)
private static (DateTime, DateTime) GetDateRange(DateTime today, int? numDays)
{
if (numDays.HasValue)
{

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

@ -220,9 +220,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas
var schemaSource = Snapshot.SchemaDef;
var schemaTarget = command.ToSchema(schemaSource.Name, schemaSource.IsSingleton);
var @events = schemaSource.Synchronize(schemaTarget, serializer, () => Snapshot.SchemaFieldsTotal + 1, options);
var events = schemaSource.Synchronize(schemaTarget, serializer, () => Snapshot.SchemaFieldsTotal + 1, options);
foreach (var @event in @events)
foreach (var @event in events)
{
RaiseEvent(SimpleMapper.Map(command, (SchemaEvent)@event));
}

2
src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs

@ -43,7 +43,7 @@ namespace Squidex.Infrastructure.States
return (existing.Doc, existing.Version);
}
return (default(T), EtagVersion.NotFound);
return (default, EtagVersion.NotFound);
}
}

4
src/Squidex.Infrastructure/EventSourcing/RetrySubscription.cs

@ -82,7 +82,7 @@ namespace Squidex.Infrastructure.EventSourcing
{
Task.Delay(ReconnectWaitMs, timerCts.Token).ContinueWith(t =>
{
dispatcher.DispatchAsync(() => Subscribe());
dispatcher.DispatchAsync(Subscribe);
}).Forget();
}
else
@ -104,7 +104,7 @@ namespace Squidex.Infrastructure.EventSourcing
public async Task StopAsync()
{
await dispatcher.DispatchAsync(() => Unsubscribe());
await dispatcher.DispatchAsync(Unsubscribe);
await dispatcher.StopAndWaitAsync();
timerCts.Cancel();

4
src/Squidex.Infrastructure/Log/LockingLogStore.cs

@ -52,7 +52,7 @@ namespace Squidex.Infrastructure.Log
break;
}
await Task.Delay(2000);
await Task.Delay(2000, cts.Token);
}
if (!cts.IsCancellationRequested)
@ -68,7 +68,7 @@ namespace Squidex.Infrastructure.Log
}
else
{
await stream.WriteAsync(LockedText, 0, LockedText.Length);
await stream.WriteAsync(LockedText, 0, LockedText.Length, cts.Token);
}
}
}

5
src/Squidex.Infrastructure/Security/Extensions.cs

@ -23,6 +23,11 @@ namespace Squidex.Infrastructure.Security
return principal.Claims.FirstOrDefault(x => x.Type == OpenIdClaims.ClientId)?.Value;
}
public static string UserOrClientId(this ClaimsPrincipal principal)
{
return principal.OpenIdSubject() ?? principal.OpenIdClientId();
}
public static string OpenIdPreferredUserName(this ClaimsPrincipal principal)
{
return principal.Claims.FirstOrDefault(x => x.Type == OpenIdClaims.PreferredUserName)?.Value;

2
src/Squidex.Infrastructure/States/IStore.cs

@ -12,7 +12,7 @@ namespace Squidex.Infrastructure.States
{
public delegate void HandleEvent(Envelope<IEvent> @event);
public delegate void HandleSnapshot<T>(T state);
public delegate void HandleSnapshot<in T>(T state);
public interface IStore<in TKey>
{

11
src/Squidex.Shared/Identity/ClaimsPrincipalExtensions.cs

@ -27,12 +27,19 @@ namespace Squidex.Shared.Identity
public static PermissionSet Permissions(this ClaimsPrincipal principal)
{
return new PermissionSet(principal.Claims.Where(x => x.Type == SquidexClaimTypes.Permissions).Select(x => new Permission(x.Value)));
return new PermissionSet(principal.Claims
.Where(x =>
x.Type == SquidexClaimTypes.Permissions ||
x.Type == SquidexClaimTypes.PermissionsClient)
.Select(x => new Permission(x.Value)));
}
public static IEnumerable<Claim> GetSquidexClaims(this ClaimsPrincipal principal)
{
return principal.Claims.Where(c => c.Type.StartsWith(SquidexClaimTypes.Prefix, StringComparison.Ordinal));
return principal.Claims
.Where(x =>
x.Type.StartsWith(SquidexClaimTypes.Prefix, StringComparison.Ordinal) ||
x.Type.StartsWith(SquidexClaimTypes.PrefixClient, StringComparison.Ordinal));
}
}
}

4
src/Squidex.Shared/Identity/SquidexClaimTypes.cs

@ -21,8 +21,12 @@ namespace Squidex.Shared.Identity
public static readonly string Permissions = "urn:squidex:permissions";
public static readonly string PermissionsClient = "client_urn:squidex:permissions";
public static readonly string Prefix = "urn:squidex:";
public static readonly string PrefixClient = "client_urn:squidex:";
public static readonly string PictureUrlStore = "store";
}
}

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

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

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

@ -57,7 +57,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
/// </returns>
[HttpGet]
[Route("assets/{id}/")]
[ProducesResponseType(200)]
[ProducesResponseType(typeof(FileResult), 200)]
[ApiCosts(0.5)]
public async Task<IActionResult> GetAssetContent(Guid id, [FromQuery] long version = EtagVersion.Any, [FromQuery] int? width = null, [FromQuery] int? height = null, [FromQuery] string mode = null)
{

2
src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs

@ -40,7 +40,7 @@ namespace Squidex.Areas.Api.Controllers.Backups
[HttpGet]
[Route("apps/{app}/backups/{id}")]
[ResponseCache(Duration = 3600 * 24 * 30)]
[ProducesResponseType(200)]
[ProducesResponseType(typeof(FileResult), 200)]
[ApiCosts(0)]
[AllowAnonymous]
public IActionResult GetBackupContent(string app, Guid id)

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

@ -21,7 +21,7 @@ using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Backups
{
/// <summary>
/// Manages backups for app.
/// Manages backups for apps.
/// </summary>
[ApiExplorerSettings(GroupName = nameof(Backups))]
public class BackupsController : ApiController

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

@ -11,12 +11,16 @@ using Orleans;
using Squidex.Areas.Api.Controllers.Backups.Models;
using Squidex.Domain.Apps.Entities.Backup;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Security;
using Squidex.Infrastructure.Orleans;
using Squidex.Pipeline;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Backups
{
/// <summary>
/// Manages backups for apps.
/// </summary>
[ApiExplorerSettings(GroupName = nameof(Backups))]
public class RestoreController : ApiController
{
private readonly IGrainFactory grainFactory;
@ -28,17 +32,18 @@ namespace Squidex.Areas.Api.Controllers.Backups
}
/// <summary>
/// Get current status.
/// Get current restore status.
/// </summary>
/// <returns>
/// 200 => Status returned.
/// </returns>
[HttpGet]
[Route("apps/restore/")]
[ProducesResponseType(typeof(RestoreJobDto), 200)]
[ApiPermission(Permissions.AdminRestoreRead)]
public async Task<IActionResult> GetJob()
{
var restoreGrain = grainFactory.GetGrain<IRestoreGrain>(User.OpenIdSubject());
var restoreGrain = grainFactory.GetGrain<IRestoreGrain>(SingleGrain.Id);
var job = await restoreGrain.GetJobAsync();
@ -64,7 +69,7 @@ namespace Squidex.Areas.Api.Controllers.Backups
[ApiPermission(Permissions.AdminRestoreCreate)]
public async Task<IActionResult> PostRestore([FromBody] RestoreRequest request)
{
var restoreGrain = grainFactory.GetGrain<IRestoreGrain>(User.OpenIdSubject());
var restoreGrain = grainFactory.GetGrain<IRestoreGrain>(SingleGrain.Id);
await restoreGrain.RestoreAsync(request.Url, request.Name);

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

@ -14,7 +14,6 @@ using Squidex.Config;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.GenerateJsonSchema;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Pipeline.Swagger;
using Squidex.Shared;

2
src/Squidex/Areas/Api/Controllers/Ping/PingController.cs

@ -53,7 +53,7 @@ namespace Squidex.Areas.Api.Controllers.Ping
[Route("ping/{app}/")]
[ApiPermission(Permissions.AppCommon)]
[ApiCosts(0)]
public IActionResult GetPing(string app)
public IActionResult GetAppPing(string app)
{
return NoContent();
}

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

@ -136,7 +136,7 @@ namespace Squidex.Areas.Api.Controllers.Users
/// </returns>
[HttpGet]
[Route("users/{id}/picture/")]
[ProducesResponseType(200)]
[ProducesResponseType(typeof(FileResult), 200)]
[ResponseCache(Duration = 3600)]
public async Task<IActionResult> GetUserPicture(string id)
{

40
src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs

@ -7,6 +7,7 @@
using System;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;
using IdentityServer4;
using IdentityServer4.Models;
@ -17,6 +18,8 @@ using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities;
using Squidex.Infrastructure;
using Squidex.Pipeline;
using Squidex.Shared;
using Squidex.Shared.Identity;
namespace Squidex.Areas.IdentityServer.Config
{
@ -25,14 +28,17 @@ namespace Squidex.Areas.IdentityServer.Config
private readonly IAppProvider appProvider;
private readonly Dictionary<string, Client> staticClients = new Dictionary<string, Client>(StringComparer.OrdinalIgnoreCase);
public LazyClientStore(IOptions<MyUrlsOptions> urlsOptions, IAppProvider appProvider)
public LazyClientStore(
IOptions<MyUrlsOptions> urlsOptions,
IOptions<MyIdentityOptions> identityOptions,
IAppProvider appProvider)
{
Guard.NotNull(urlsOptions, nameof(urlsOptions));
Guard.NotNull(appProvider, nameof(appProvider));
this.appProvider = appProvider;
CreateStaticClients(urlsOptions);
CreateStaticClients(urlsOptions, identityOptions);
}
public async Task<Client> FindClientByIdAsync(string clientId)
@ -83,15 +89,15 @@ namespace Squidex.Areas.IdentityServer.Config
};
}
private void CreateStaticClients(IOptions<MyUrlsOptions> urlsOptions)
private void CreateStaticClients(IOptions<MyUrlsOptions> urlsOptions, IOptions<MyIdentityOptions> identityOptions)
{
foreach (var client in CreateStaticClients(urlsOptions.Value))
foreach (var client in CreateStaticClients(urlsOptions.Value, identityOptions.Value))
{
staticClients[client.ClientId] = client;
}
}
private static IEnumerable<Client> CreateStaticClients(MyUrlsOptions urlsOptions)
private static IEnumerable<Client> CreateStaticClients(MyUrlsOptions urlsOptions, MyIdentityOptions identityOptions)
{
var frontendId = Constants.FrontendClient;
@ -150,6 +156,30 @@ namespace Squidex.Areas.IdentityServer.Config
},
RequireConsent = false
};
if (identityOptions.IsAdminClientConfigured())
{
var id = identityOptions.AdminClientId;
yield return new Client
{
ClientId = id,
ClientName = id,
ClientSecrets = new List<Secret> { new Secret(identityOptions.AdminClientSecret.Sha256()) },
AccessTokenLifetime = (int)TimeSpan.FromDays(30).TotalSeconds,
AllowedGrantTypes = GrantTypes.ClientCredentials,
AllowedScopes = new List<string>
{
Constants.ApiScope,
Constants.RoleScope,
Constants.PermissionsScope
},
Claims = new List<Claim>
{
new Claim(SquidexClaimTypes.Permissions, Permissions.Admin)
}
};
}
}
}
}

9
src/Squidex/Config/Logging.cs

@ -5,6 +5,8 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
#define LOG_ALL_IDENTITY_SERVER_NONE
using System;
using Microsoft.Extensions.Logging;
@ -40,7 +42,12 @@ namespace Squidex.Config
{
return level > LogLevel.Information;
}
#if LOG_ALL_IDENTITY_SERVER
if (category.StartsWith("IdentityServer4.", StringComparison.OrdinalIgnoreCase))
{
return true;
}
#endif
return level >= LogLevel.Information;
});
}

9
src/Squidex/Config/MyIdentityOptions.cs

@ -13,6 +13,10 @@ namespace Squidex.Config
public string AdminPassword { get; set; }
public string AdminClientId { get; set; }
public string AdminClientSecret { get; set; }
public string GithubClient { get; set; }
public string GithubSecret { get; set; }
@ -48,6 +52,11 @@ namespace Squidex.Config
return !string.IsNullOrWhiteSpace(AdminEmail) && !string.IsNullOrWhiteSpace(AdminPassword);
}
public bool IsAdminClientConfigured()
{
return !string.IsNullOrWhiteSpace(AdminClientId) && !string.IsNullOrWhiteSpace(AdminClientSecret);
}
public bool IsOidcConfigured()
{
return !string.IsNullOrWhiteSpace(OidcAuthority) && !string.IsNullOrWhiteSpace(OidcClient) && !string.IsNullOrWhiteSpace(OidcSecret);

11
src/Squidex/Pipeline/Extensions.cs

@ -29,12 +29,17 @@ namespace Squidex.Pipeline
{
var parts = clientId.Split(':', '~');
if (parts.Length != 2)
if (parts.Length == 1)
{
return (null, null);
return (null, parts[0]);
}
return (parts[0], parts[1]);
if (parts.Length == 2)
{
return (parts[0], parts[1]);
}
return (null, null);
}
}
}

9
src/Squidex/Pipeline/Squid/SquidMiddleware.cs

@ -63,14 +63,9 @@ namespace Squidex.Pipeline.Squid
background = backgroundValue;
}
var isSmall = false;
var isSmall = request.Query.TryGetValue("small", out _);
if (request.Query.TryGetValue("small", out _))
{
isSmall = true;
}
var svg = string.Empty;
string svg;
if (isSmall)
{

2
src/Squidex/app/features/assets/pages/assets-page.component.ts

@ -5,8 +5,6 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
// tslint:disable:prefer-for-of
import { Component, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import { onErrorResumeNext } from 'rxjs/operators';

5
src/Squidex/app/features/content/shared/assets-editor.component.html

@ -1,8 +1,9 @@
<div class="assets-container" [class.disabled]="snapshot.isDisabled" (paste)="pasteFiles($event)" tabindex="1000">
<div class="assets-container" [class.disabled]="snapshot.isDisabled" (sqxFileDrop)="addFiles($event)" tabindex="1000">
<div class="header list">
<div class="row no-gutters">
<div class="col">
<div class="drop-area align-items-center" (click)="assetsDialog.show()">
<div class="drop-area align-items-center" (click)="assetsDialog.show()" (sqxFileDrop)="addFiles($event)">
Drop files or click here to add assets.
</div>
</div>

22
src/Squidex/app/features/content/shared/assets-editor.component.ts

@ -5,8 +5,6 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
// tslint:disable:prefer-for-of
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, OnInit } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
@ -104,23 +102,9 @@ export class AssetsEditorComponent extends StatefulControlComponent<State, strin
this.next(s => ({ ...s, assets }));
}
public pasteFiles(event: ClipboardEvent) {
for (let i = 0; i < event.clipboardData.items.length; i++) {
const file = event.clipboardData.items[i].getAsFile();
if (file) {
this.next(s => ({ ...s, assetFiles: s.assetFiles.pushFront(file) }));
}
}
}
public addFiles(files: FileList) {
for (let i = 0; i < files.length; i++) {
const file = files[i];
if (file) {
this.next(s => ({ ...s, assetFiles: s.assetFiles.pushFront(file) }));
}
public addFiles(files: File[]) {
for (let file of files) {
this.next(s => ({ ...s, assetFiles: s.assetFiles.pushFront(file) }));
}
}

2
src/Squidex/app/features/content/shared/references-editor.component.ts

@ -5,8 +5,6 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
// tslint:disable:prefer-for-of
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, Input, OnInit } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms';

2
src/Squidex/app/features/rules/pages/rules/triggers/asset-changed-trigger.component.html

@ -4,7 +4,7 @@
<sqx-control-errors for="condition" [submitted]="triggerFormSubmitted"></sqx-control-errors>
<textarea class="form-control code" id="condition" formControlName="condition"></textarea>
<textarea class="form-control code" id="condition" formControlName="condition" placeholder="Optional condition as javascript expression"></textarea>
</div>
<div class="help">

2
src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.html

@ -21,7 +21,7 @@
<span class="truncate">{{schema.schema.displayName}}</span>
</td>
<td class="text-center">
<input type="text" class="form-control code" [(ngModel)]="schema.condition" (blur)="updateValue()" />
<input type="text" class="form-control code" [(ngModel)]="schema.condition" (blur)="updateValue()" placeholder="Optional condition as javascript expression" />
</td>
<td class="text-center">
<button type="button" class="btn btn-text-secondary" (click)="removeSchema(schema)">

56
src/Squidex/app/framework/angular/forms/file-drop.directive.ts

@ -5,18 +5,33 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Directive, ElementRef, EventEmitter, HostListener, Output, Renderer2 } from '@angular/core';
// tslint:disable:prefer-for-of
import { Directive, ElementRef, EventEmitter, HostListener, Input, Output, Renderer2 } from '@angular/core';
import { Types } from './../../utils/types';
const ImageTypes = [
'image/jpeg',
'image/png',
'image/jpg',
'image/gif'
];
@Directive({
selector: '[sqxFileDrop]'
})
export class FileDropDirective {
private dragCounter = 0;
@Input()
public allowedFiles: string[];
@Input()
public onlyImages: boolean;
@Output('sqxFileDrop')
public drop = new EventEmitter<FileList>();
public drop = new EventEmitter<File[]>();
constructor(
private readonly element: ElementRef,
@ -24,6 +39,25 @@ export class FileDropDirective {
) {
}
@HostListener('paste', ['$event'])
public onPaste(event: ClipboardEvent) {
const result: File[] = [];
for (let i = 0; i < event.clipboardData.items.length; i++) {
const file = event.clipboardData.items[i].getAsFile();
if (this.isAllowedFile(file)) {
result.push(file!);
}
}
if (result.length > 0) {
this.drop.emit(result);
}
this.stopEvent(event);
}
@HostListener('dragend', ['$event'])
@HostListener('dragleave', ['$event'])
public onDragEnd(event: DragDropEvent) {
@ -57,7 +91,19 @@ export class FileDropDirective {
const hasFiles = this.hasFiles(event.dataTransfer.types);
if (hasFiles) {
this.drop.emit(event.dataTransfer.files);
const result: File[] = [];
for (let i = 0; i < event.dataTransfer.files.length; i++) {
const file = event.dataTransfer.files.item(i);
if (this.isAllowedFile(file)) {
result.push(file!);
}
}
if (result.length > 0) {
this.drop.emit(result);
}
this.dragEnd(0);
this.stopEvent(event);
@ -85,6 +131,10 @@ export class FileDropDirective {
}
}
private isAllowedFile(file: File | null) {
return file && (!this.allowedFiles || this.allowedFiles.indexOf(file.type) >= 0) && (!this.onlyImages || ImageTypes.indexOf(file.type) >= 0);
}
private hasFiles(types: any): boolean {
if (!types) {
return false;

4
src/Squidex/app/shared/components/assets-list.component.html

@ -1,4 +1,4 @@
<div class="file-drop" (sqxFileDrop)="addFiles($event)" *ngIf="!isDisabled" (paste)="pasteFiles($event)">
<div class="file-drop" (sqxFileDrop)="addFiles($event)" *ngIf="!isDisabled">
<h3 class="file-drop-header">Drop files here to upload</h3>
<div class="file-drop-or">or</div>
@ -14,7 +14,7 @@
<div class="file-drop-info">Drop file on existing item to replace the asset with a newer version.</div>
</div>
<div class="row assets" [class.unrow]="isListView" *ngIf="state.tagsNames | async; let tags" (paste)="pasteFiles($event)">
<div class="row assets" [class.unrow]="isListView" *ngIf="state.tagsNames | async; let tags" (paste)="addFiles($event)">
<sqx-asset *ngFor="let file of newFiles" [initFile]="file"
[isListView]="isListView"
(failed)="remove(file)"

22
src/Squidex/app/shared/components/assets-list.component.ts

@ -5,8 +5,6 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
// tslint:disable:prefer-for-of
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { onErrorResumeNext } from 'rxjs/operators';
@ -78,23 +76,9 @@ export class AssetsListComponent {
this.newFiles = this.newFiles.remove(file);
}
public pasteFiles(event: ClipboardEvent) {
for (let i = 0; i < event.clipboardData.items.length; i++) {
const file = event.clipboardData.items[i].getAsFile();
if (file) {
this.newFiles = this.newFiles.pushFront(file);
}
}
}
public addFiles(files: FileList) {
for (let i = 0; i < files.length; i++) {
const file = files[i];
if (file) {
this.newFiles = this.newFiles.pushFront(file);
}
public addFiles(files: File[]) {
for (let file of files) {
this.newFiles = this.newFiles.pushFront(file);
}
return true;

4
src/Squidex/app/shared/components/assets-selector.component.ts

@ -5,8 +5,6 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
// tslint:disable:prefer-for-of
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, OnInit, Output } from '@angular/core';
import { onErrorResumeNext } from 'rxjs/operators';
@ -40,7 +38,7 @@ export class AssetsSelectorComponent extends StatefulComponent<State> implements
constructor(changeDector: ChangeDetectorRef,
public readonly assetsState: AssetsDialogState,
private readonly localStore: LocalStoreService
public readonly localStore: LocalStoreService
) {
super(changeDector, {
selectedAssets: {},

6
src/Squidex/app/shared/components/markdown-editor.component.html

@ -1,11 +1,7 @@
<div #container class="drop-container">
<div #container class="drop-container" (sqxFileDrop)="insertFiles($event)" [onlyImages]="true">
<div #inner [class.fullscreen]="snapshot.isFullscreen">
<textarea class="form-control" #editor></textarea>
</div>
<div class="file-drop drag drop-area">
<div class="drop-text">Drop assets here to add them.</div>
</div>
</div>
<ng-container *sqxModalView="assetsDialog;onRoot:true;closeAuto:false">

47
src/Squidex/app/shared/components/markdown-editor.component.ts

@ -9,7 +9,11 @@ import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, E
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import {
AppsState,
AssetDto,
AssetsService,
AuthService,
DateTime,
DialogModel,
ResourceLoaderService,
StatefulControlComponent,
@ -50,6 +54,9 @@ export class MarkdownEditorComponent extends StatefulControlComponent<State, str
public assetsDialog = new DialogModel();
constructor(changeDetector: ChangeDetectorRef,
private readonly appsState: AppsState,
private readonly assetsService: AssetsService,
private readonly authState: AuthService,
private readonly renderer: Renderer2,
private readonly resourceLoader: ResourceLoaderService
) {
@ -207,4 +214,44 @@ export class MarkdownEditorComponent extends StatefulControlComponent<State, str
this.assetsDialog.hide();
}
public insertFiles(files: File[]) {
const doc = this.simplemde.codemirror.getDoc();
for (let file of files) {
this.uploadFile(doc, file);
}
}
private uploadFile(doc: any, file: File) {
const uploadCursor = doc.getCursor();
const uploadText = `![Uploading file...${new Date()}]()`;
doc.replaceSelection(uploadText);
const replaceText = (replacement: string) => {
const cursor = doc.getCursor();
const text = doc.getValue().replace(uploadText, replacement);
doc.setValue(text);
if (uploadCursor && uploadCursor.line === cursor.line) {
const offset = replacement.length - uploadText.length;
doc.setCursor({ line: cursor.line, ch: cursor.ch + offset });
} else {
doc.setCursor(cursor);
}
};
this.assetsService.uploadFile(this.appsState.appName, file, this.authState.user!.token, DateTime.now())
.subscribe(asset => {
if (Types.is(asset, AssetDto)) {
replaceText(`![${asset.fileName}](${asset.url} '${asset.fileName}')`);
}
}, () => {
replaceText('FAILED');
});
}
}

6
src/Squidex/app/shared/components/rich-editor.component.html

@ -1,9 +1,5 @@
<div class="drop-container">
<div class="drop-container" (sqxFileDrop)="insertFiles($event)" [onlyImages]="true">
<div class="editor" #editor></div>
<div class="file-drop drag drop-area">
<div class="drop-text">Drop assets here to add them.</div>
</div>
</div>
<ng-container *sqxModalView="assetsDialog;onRoot:true;closeAuto:false">

84
src/Squidex/app/shared/components/rich-editor.component.ts

@ -5,11 +5,17 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
// tslint:disable:prefer-for-of
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, forwardRef, OnDestroy, Output, ViewChild } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import {
AppsState,
AssetDto,
AssetsService,
AuthService,
DateTime,
DialogModel,
ExternalControlComponent,
ResourceLoaderService,
@ -22,6 +28,13 @@ export const SQX_RICH_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => RichEditorComponent), multi: true
};
const ImageTypes = [
'image/jpeg',
'image/png',
'image/jpg',
'image/gif'
];
@Component({
selector: 'sqx-rich-editor',
styleUrls: ['./rich-editor.component.scss'],
@ -44,6 +57,9 @@ export class RichEditorComponent extends ExternalControlComponent<string> implem
public assetsDialog = new DialogModel();
constructor(changeDetector: ChangeDetectorRef,
private readonly appsState: AppsState,
private readonly assetsService: AssetsService,
private readonly authState: AuthService,
private readonly resourceLoader: ResourceLoaderService
) {
super(changeDetector);
@ -60,7 +76,7 @@ export class RichEditorComponent extends ExternalControlComponent<string> implem
public ngAfterViewInit() {
const self = this;
this.resourceLoader.loadScript('https://cdnjs.cloudflare.com/ajax/libs/tinymce/4.5.4/tinymce.min.js').then(() => {
this.resourceLoader.loadScript('https://cdnjs.cloudflare.com/ajax/libs/tinymce/4.9.3/tinymce.min.js').then(() => {
tinymce.init(self.getEditorOptions());
});
}
@ -75,11 +91,24 @@ export class RichEditorComponent extends ExternalControlComponent<string> implem
return {
convert_fonts_to_spans: true,
convert_urls: false,
plugins: 'code image media link lists advlist',
plugins: 'code image media link lists advlist paste',
removed_menuitems: 'newdocument',
resize: true,
theme: 'modern',
toolbar: 'undo redo | styleselect | bold italic | alignleft aligncenter | bullist numlist outdent indent | link image media | assets',
images_upload_handler: (blob: any, success: (url: string) => void, failed: () => void) => {
const file = new File([blob.blob()], blob.filename(), { lastModified: new Date().getTime() });
this.assetsService.uploadFile(this.appsState.appName, file, this.authState.user!.token, DateTime.now())
.subscribe(asset => {
if (Types.is(asset, AssetDto)) {
success(asset.url);
}
}, () => {
failed();
});
},
setup: (editor: any) => {
self.tinyEditor = editor;
self.tinyEditor.setMode(this.isDisabled ? 'readonly' : 'design');
@ -101,6 +130,28 @@ export class RichEditorComponent extends ExternalControlComponent<string> implem
}
});
self.tinyEditor.on('paste', (event: ClipboardEvent) => {
for (let i = 0; i < event.clipboardData.items.length; i++) {
const file = event.clipboardData.items[i].getAsFile();
if (file && ImageTypes.indexOf(file.type) >= 0) {
self.uploadFile(file);
}
}
});
self.tinyEditor.on('drop', (event: DragEvent) => {
if (event.dataTransfer) {
for (let i = 0; i < event.dataTransfer.files.length; i++) {
const file = event.dataTransfer.files.item(i);
if (file && ImageTypes.indexOf(file.type) >= 0) {
self.uploadFile(file);
}
}
}
});
self.tinyEditor.on('blur', () => {
self.callTouched();
});
@ -144,4 +195,31 @@ export class RichEditorComponent extends ExternalControlComponent<string> implem
this.assetsDialog.hide();
}
public insertFiles(files: File[]) {
for (let file of files) {
this.uploadFile(file);
}
}
private uploadFile(file: File) {
const uploadText = `[Uploading file...${new Date()}]`;
this.tinyEditor.execCommand('mceInsertContent', false, uploadText);
const replaceText = (replacement: string) => {
const content = this.tinyEditor.getContent().replace(uploadText, replacement);
this.tinyEditor.setContent(content);
};
this.assetsService.uploadFile(this.appsState.appName, file, this.authState.user!.token, DateTime.now())
.subscribe(asset => {
if (Types.is(asset, AssetDto)) {
replaceText(`<img src="${asset.url}" alt="${asset.fileName}" />`);
}
}, () => {
replaceText('FAILED');
});
}
}

1
src/Squidex/app/shared/state/contents.forms.ts

@ -5,7 +5,6 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
// tslint:disable:prefer-for-of
import { FormArray, FormBuilder, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms';

10
src/Squidex/appsettings.json

@ -263,6 +263,16 @@
* Enable password auth. Set this to false if you want to disable local login, leaving only 3rd party login options.
*/
"allowPasswordAuth": true,
/*
* Initial admin user.
*/
"adminEmail": "",
"adminPassword": "",
/*
* Client with all admin permissions.
*/
"adminClientId": "",
"adminClientSecret": "",
/*
* Settings for Google auth (keep empty to disable).
*/

2
tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/AssertHelper.cs

@ -35,7 +35,7 @@ namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization
{
lhs.Should().BeOfType(rhs.GetType());
((object)lhs).Should().BeEquivalentTo(rhs, o => o.IncludingAllRuntimeProperties().Excluding((IMemberInfo x) => x.SelectedMemberPath == "Properties.IsFrozen"));
((object)lhs).Should().BeEquivalentTo(rhs, o => o.IncludingAllRuntimeProperties().Excluding(x => x.SelectedMemberPath == "Properties.IsFrozen"));
}
public static void ShouldBeSameEventType(this IEvent lhs, IEvent rhs)

1
tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs

@ -35,7 +35,6 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
private readonly string actionDescription = "MyDescription";
private readonly Guid ruleId = Guid.NewGuid();
private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app");
private readonly NamedId<Guid> schemaId = NamedId.Of(Guid.NewGuid(), "my-schema");
private readonly TypeNameRegistry typeNameRegistry = new TypeNameRegistry();
private readonly RuleService sut;

2
tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentGrainTests.cs

@ -79,7 +79,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
Change = "<change-script>",
Create = "<create-script>",
Delete = "<delete-script>",
Update = "<update-script>",
Update = "<update-script>"
};
var schemaDef =

Loading…
Cancel
Save