Browse Source

Client Key management

pull/1/head
Sebastian 10 years ago
parent
commit
b5a0d64169
  1. 2
      Squidex.sln.DotSettings
  2. 22
      src/Squidex.Events/Apps/AppClientKeyCreated.cs
  3. 22
      src/Squidex.Events/Apps/AppClientKeyRevoked.cs
  4. 20
      src/Squidex.Events/Apps/AppLanguagesConfigured.cs
  5. 42
      src/Squidex.Infrastructure/CQRS/Commands/EnrichWithTimestampHandler.cs
  6. 17
      src/Squidex.Infrastructure/CQRS/Commands/ITimestampCommand.cs
  7. 40
      src/Squidex.Infrastructure/Json/LanguageConverter.cs
  8. 19
      src/Squidex.Read/Apps/IAppClientKeyEntity.cs
  9. 5
      src/Squidex.Read/Apps/IAppEntity.cs
  10. 6
      src/Squidex.Read/Apps/Services/Implementations/CachingAppProvider.cs
  11. 3
      src/Squidex.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs
  12. 25
      src/Squidex.Store.MongoDb/Apps/MongoAppClientKeyEntity.cs
  13. 22
      src/Squidex.Store.MongoDb/Apps/MongoAppEntity.cs
  14. 15
      src/Squidex.Store.MongoDb/Apps/MongoAppRepository.cs
  15. 15
      src/Squidex.Write/Apps/AppCommandHandler.cs
  16. 80
      src/Squidex.Write/Apps/AppDomainObject.cs
  17. 33
      src/Squidex.Write/Apps/ClientKeyGenerator.cs
  18. 26
      src/Squidex.Write/Apps/Commands/ConfigureLanguages.cs
  19. 30
      src/Squidex.Write/Apps/Commands/CreateClientKey.cs
  20. 26
      src/Squidex.Write/Apps/Commands/RevokeClientKey.cs
  21. 1
      src/Squidex/Configurations/Domain/Serializers.cs
  22. 8
      src/Squidex/Configurations/Domain/WriteModule.cs
  23. 58
      src/Squidex/Configurations/Identity/LazyClientStore.cs
  24. 65
      src/Squidex/Modules/Api/Apps/AppClientKeysController.cs
  25. 5
      src/Squidex/Modules/Api/Apps/AppContributorsController.cs
  26. 60
      src/Squidex/Modules/Api/Apps/AppLanguagesController.cs
  27. 2
      src/Squidex/Modules/Api/Apps/Models/AssignContributorDto.cs
  28. 15
      src/Squidex/Modules/Api/Apps/Models/ClientKeyCreatedDto.cs
  29. 19
      src/Squidex/Modules/Api/Apps/Models/ClientKeyDto.cs
  30. 18
      src/Squidex/Modules/Api/Apps/Models/ConfigureLanguagesDto.cs
  31. 6
      src/Squidex/Modules/Api/Apps/Models/ContributorDto.cs
  32. 1
      src/Squidex/Modules/Api/Apps/Models/CreateAppDto.cs
  33. 17
      src/Squidex/Modules/Api/LanguageDto.cs
  34. 3
      src/Squidex/Modules/Api/Languages/LanguagesController.cs
  35. 26
      src/Squidex/Pipeline/AppFilterAttribute.cs
  36. 7
      src/Squidex/app/app.module.ts
  37. 26
      src/Squidex/app/components/internal/app/settings/credentials-page.component.html
  38. 11
      src/Squidex/app/components/internal/app/settings/credentials-page.component.scss
  39. 25
      src/Squidex/app/components/internal/app/settings/credentials-page.component.ts
  40. 40
      src/Squidex/app/components/internal/app/settings/languages-page.component.html
  41. 14
      src/Squidex/app/components/internal/app/settings/languages-page.component.scss
  42. 54
      src/Squidex/app/components/internal/app/settings/languages-page.component.ts
  43. 3
      src/Squidex/app/components/internal/module.ts
  44. 2
      src/Squidex/app/shared/index.ts
  45. 52
      src/Squidex/app/shared/services/app-client-keys.service.ts
  46. 1
      src/Squidex/app/shared/services/app-contributors.service.ts
  47. 76
      src/Squidex/app/shared/services/app-languages.service.spec.ts
  48. 40
      src/Squidex/app/shared/services/app-languages.service.ts
  49. 1
      src/Squidex/app/shared/services/apps.service.ts
  50. 1
      src/Squidex/app/shared/services/languages.service.ts
  51. 1
      src/Squidex/app/shared/services/users.service.ts
  52. 1
      src/Squidex/app/theme/_layout.scss
  53. 0
      src/Squidex/app/theme/_lib-completer.scss
  54. 24
      src/Squidex/app/theme/_lib-dnd.scss
  55. 3
      src/Squidex/app/theme/theme.scss
  56. 1
      src/Squidex/package.json
  57. 51
      tests/Squidex.Infrastructure.Tests/CQRS/Commands/EnrichWithTimestampHandlerTests.cs
  58. 29
      tests/Squidex.Infrastructure.Tests/LanguageTests.cs
  59. 71
      tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs
  60. 149
      tests/Squidex.Write.Tests/Apps/AppDomainObjectTests.cs
  61. 26
      tests/Squidex.Write.Tests/Apps/ClientKeyGeneratorTests.cs

2
Squidex.sln.DotSettings

@ -40,7 +40,7 @@
<s:String x:Key="/Default/CodeStyle/CodeCleanup/Profiles/=Typescript/@EntryIndexedValue">&lt;?xml version="1.0" encoding="utf-16"?&gt;&lt;Profile name="Typescript"&gt;&lt;JsInsertSemicolon&gt;True&lt;/JsInsertSemicolon&gt;&lt;FormatAttributeQuoteDescriptor&gt;True&lt;/FormatAttributeQuoteDescriptor&gt;&lt;CorrectVariableKindsDescriptor&gt;True&lt;/CorrectVariableKindsDescriptor&gt;&lt;VariablesToInnerScopesDescriptor&gt;True&lt;/VariablesToInnerScopesDescriptor&gt;&lt;StringToTemplatesDescriptor&gt;True&lt;/StringToTemplatesDescriptor&gt;&lt;RemoveRedundantQualifiersTs&gt;True&lt;/RemoveRedundantQualifiersTs&gt;&lt;OptimizeImportsTs&gt;True&lt;/OptimizeImportsTs&gt;&lt;/Profile&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/CodeCleanup/SilentCleanupProfile/@EntryValue"></s:String>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/JavaScriptCodeFormatting/QUOTE_STYLE/@EntryValue">SingleQuoted</s:String>
<s:String x:Key="/Default/CodeStyle/FileHeader/FileHeaderText/@EntryValue">=========================================================================&#xD;
<s:String x:Key="/Default/CodeStyle/FileHeader/FileHeaderText/@EntryValue">==========================================================================&#xD;
$FILENAME$&#xD;
Squidex Headless CMS&#xD;
==========================================================================&#xD;

22
src/Squidex.Events/Apps/AppClientKeyCreated.cs

@ -0,0 +1,22 @@
// ==========================================================================
// AppClientKeyCreated.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events;
namespace Squidex.Events.Apps
{
[TypeName("AppClientKeyCreated")]
public sealed class AppClientKeyCreated : IEvent
{
public string ClientKey { get; set; }
public DateTime ExpiresUtc { get; set; }
}
}

22
src/Squidex.Events/Apps/AppClientKeyRevoked.cs

@ -0,0 +1,22 @@
// ==========================================================================
// AppClientKeyRevoked.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events;
namespace Squidex.Events.Apps
{
[TypeName("AppClientKeyRevoked")]
public sealed class AppClientKeyRevoked : IEvent
{
public string ClientKey { get; set; }
public DateTime ExpiresUtc { get; set; }
}
}

20
src/Squidex.Events/Apps/AppLanguagesConfigured.cs

@ -0,0 +1,20 @@
// ==========================================================================
// AppLanguagesConfigured.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events;
namespace Squidex.Events.Apps
{
[TypeName("AppLanguagesConfigured")]
public sealed class AppLanguagesConfigured : IEvent
{
public List<Language> Languages { get; set; }
}
}

42
src/Squidex.Infrastructure/CQRS/Commands/EnrichWithTimestampHandler.cs

@ -0,0 +1,42 @@
// ==========================================================================
// EnrichWithTimestampHandler.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Threading.Tasks;
namespace Squidex.Infrastructure.CQRS.Commands
{
public sealed class EnrichWithTimestampHandler : ICommandHandler
{
private readonly Func<DateTime> timestamp;
public EnrichWithTimestampHandler()
: this(() => DateTime.UtcNow)
{
}
public EnrichWithTimestampHandler(Func<DateTime> timestamp)
{
Guard.NotNull(timestamp, nameof(timestamp));
this.timestamp = timestamp;
}
public Task<bool> HandleAsync(CommandContext context)
{
var timestampCommand = context.Command as ITimestampCommand;
if (timestampCommand != null)
{
timestampCommand.Timestamp = timestamp();
}
return Task.FromResult(false);
}
}
}

17
src/Squidex.Infrastructure/CQRS/Commands/ITimestampCommand.cs

@ -0,0 +1,17 @@
// ==========================================================================
// ITimestampCommand.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
namespace Squidex.Infrastructure.CQRS.Commands
{
public interface ITimestampCommand : ICommand
{
DateTime Timestamp { get; set; }
}
}

40
src/Squidex.Infrastructure/Json/LanguageConverter.cs

@ -0,0 +1,40 @@
// =========================================================================
// LanguageConverter.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using Newtonsoft.Json;
namespace Squidex.Infrastructure.Json
{
public sealed class LanguageConverter : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var language = value as Language;
if (language != null)
{
writer.WriteValue(language.Iso2Code);
}
else
{
writer.WriteNull();
}
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
return reader.TokenType == JsonToken.Null ? null : Language.GetLanguage((string) reader.Value);
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof(Language);
}
}
}

19
src/Squidex.Read/Apps/IAppClientKeyEntity.cs

@ -0,0 +1,19 @@
// ==========================================================================
// IAppClientKeyEntity.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
namespace Squidex.Read.Apps
{
public interface IAppClientKeyEntity
{
string ClientKey { get; }
DateTime ExpiresUtc { get; }
}
}

5
src/Squidex.Read/Apps/IAppEntity.cs

@ -7,6 +7,7 @@
// ==========================================================================
using System.Collections.Generic;
using Squidex.Infrastructure;
namespace Squidex.Read.Apps
{
@ -14,6 +15,10 @@ namespace Squidex.Read.Apps
{
string Name { get; }
IEnumerable<Language> Languages { get; }
IEnumerable<IAppClientKeyEntity> ClientKeys { get; }
IEnumerable<IAppContributorEntity> Contributors { get; }
}
}

6
src/Squidex.Read/Apps/Services/Implementations/CachingAppProvider.cs

@ -64,7 +64,11 @@ namespace Squidex.Read.Apps.Services.Implementations
public Task On(Envelope<IEvent> @event)
{
if (@event.Payload is AppContributorAssigned || @event.Payload is AppContributorRemoved)
if (@event.Payload is AppContributorAssigned ||
@event.Payload is AppContributorRemoved ||
@event.Payload is AppClientKeyCreated ||
@event.Payload is AppClientKeyRevoked ||
@event.Payload is AppLanguagesConfigured)
{
var appName = Cache.Get<string>(BuildNamesCacheKey(@event.Headers.AggregateId()));

3
src/Squidex.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs

@ -64,7 +64,8 @@ namespace Squidex.Read.Schemas.Services.Implementations
public Task On(Envelope<IEvent> @event)
{
if (@event.Payload is SchemaUpdated || @event.Payload is SchemaDeleted)
if (@event.Payload is SchemaUpdated ||
@event.Payload is SchemaDeleted)
{
var oldName = Cache.Get<string>(BuildNamesCacheKey(@event.Headers.AggregateId()));

25
src/Squidex.Store.MongoDb/Apps/MongoAppClientKeyEntity.cs

@ -0,0 +1,25 @@
// ==========================================================================
// MongoAppClientKeyEntity.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using MongoDB.Bson.Serialization.Attributes;
using Squidex.Read.Apps;
namespace Squidex.Store.MongoDb.Apps
{
public class MongoAppClientKeyEntity : IAppClientKeyEntity
{
[BsonRequired]
[BsonElement]
public string ClientKey { get; set; }
[BsonRequired]
[BsonElement]
public DateTime ExpiresUtc { get; set; }
}
}

22
src/Squidex.Store.MongoDb/Apps/MongoAppEntity.cs

@ -7,7 +7,9 @@
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using MongoDB.Bson.Serialization.Attributes;
using Squidex.Infrastructure;
using Squidex.Read.Apps;
using Squidex.Store.MongoDb.Utils;
@ -19,10 +21,28 @@ namespace Squidex.Store.MongoDb.Apps
[BsonElement]
public string Name { get; set; }
[BsonRequired]
[BsonElement]
public List<string> Languages { get; set; }
[BsonRequired]
[BsonElement]
public List<MongoAppClientKeyEntity> ClientKeys { get; set; }
[BsonRequired]
[BsonElement]
public List<MongoAppContributorEntity> Contributors { get; set; }
IEnumerable<Language> IAppEntity.Languages
{
get { return Languages.Select(Language.GetLanguage); }
}
IEnumerable<IAppClientKeyEntity> IAppEntity.ClientKeys
{
get { return ClientKeys; }
}
IEnumerable<IAppContributorEntity> IAppEntity.Contributors
{
get { return Contributors; }
@ -31,6 +51,8 @@ namespace Squidex.Store.MongoDb.Apps
public MongoAppEntity()
{
Contributors = new List<MongoAppContributorEntity>();
ClientKeys = new List<MongoAppClientKeyEntity>();
}
}
}

15
src/Squidex.Store.MongoDb/Apps/MongoAppRepository.cs

@ -64,6 +64,21 @@ namespace Squidex.Store.MongoDb.Apps
return Collection.UpdateAsync(headers, a => a.Contributors.RemoveAll(c => c.ContributorId == @event.ContributorId));
}
public Task On(AppLanguagesConfigured @event, EnvelopeHeaders headers)
{
return Collection.UpdateAsync(headers, a => a.Languages = @event.Languages.Select(x => x.Iso2Code).ToList());
}
public Task On(AppClientKeyCreated @event, EnvelopeHeaders headers)
{
return Collection.UpdateAsync(headers, a => a.ClientKeys.Add(SimpleMapper.Map(@event, new MongoAppClientKeyEntity())));
}
public Task On(AppClientKeyRevoked @event, EnvelopeHeaders headers)
{
return Collection.UpdateAsync(headers, a => a.ClientKeys.RemoveAll(c => c.ClientKey == @event.ClientKey));
}
public Task On(AppContributorAssigned @event, EnvelopeHeaders headers)
{
return Collection.UpdateAsync(headers, a =>

15
src/Squidex.Write/Apps/AppCommandHandler.cs

@ -70,6 +70,21 @@ namespace Squidex.Write.Apps
return UpdateAsync(command, x => x.RemoveContributor(command));
}
public Task On(CreateClientKey command)
{
return UpdateAsync(command, x => x.CreateClientKey(command));
}
public Task On(RevokeClientKey command)
{
return UpdateAsync(command, x => x.RevokeClientKey(command));
}
public Task On(ConfigureLanguages command)
{
return UpdateAsync(command, x => x.ConfigureLanguages(command));
}
public override Task<bool> HandleAsync(CommandContext context)
{
return context.IsHandled ? Task.FromResult(false) : this.DispatchActionAsync(context.Command);

80
src/Squidex.Write/Apps/AppDomainObject.cs

@ -16,6 +16,7 @@ using Squidex.Infrastructure.Dispatching;
using Squidex.Write.Apps.Commands;
using System.Collections.Generic;
using System.Linq;
using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Infrastructure.Reflection;
// ReSharper disable InvertIf
@ -23,6 +24,8 @@ namespace Squidex.Write.Apps
{
public sealed class AppDomainObject : DomainObject
{
private static readonly List<Language> DefaultLanguages = new List<Language> { Language.GetLanguage("en") };
private readonly HashSet<string> clientKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, PermissionLevel> contributors = new Dictionary<string, PermissionLevel>();
private string name;
@ -36,7 +39,8 @@ namespace Squidex.Write.Apps
get { return contributors; }
}
public AppDomainObject(Guid id, int version) : base(id, version)
public AppDomainObject(Guid id, int version)
: base(id, version)
{
}
@ -55,6 +59,16 @@ namespace Squidex.Write.Apps
contributors.Remove(@event.ContributorId);
}
public void On(AppClientKeyCreated @event)
{
clientKeys.Add(@event.ClientKey);
}
public void On(AppClientKeyRevoked @event)
{
clientKeys.Remove(@event.ClientKey);
}
protected override void DispatchEvent(Envelope<IEvent> @event)
{
this.DispatchAction(@event.Payload);
@ -67,7 +81,9 @@ namespace Squidex.Write.Apps
VerifyNotCreated();
RaiseEvent(SimpleMapper.Map(command, new AppCreated()));
RaiseEvent(SimpleMapper.Map(command, new AppContributorAssigned { ContributorId = command.SubjectId, Permission = PermissionLevel.Owner }));
RaiseEvent(CreateInitialOwner(command));
RaiseEvent(CreateInitialLanguage());
return this;
}
@ -77,7 +93,7 @@ namespace Squidex.Write.Apps
Guard.Valid(command, nameof(command), () => "Cannot assign contributor");
VerifyCreated();
VerifyHasStillOwner(c => c[command.ContributorId] = command.Permission);
VerifyOwnership(c => c[command.ContributorId] = command.Permission);
RaiseEvent(SimpleMapper.Map(command, new AppContributorAssigned()));
@ -90,13 +106,57 @@ namespace Squidex.Write.Apps
VerifyCreated();
VerifyContributorFound(command);
VerifyHasStillOwner(c => c.Remove(command.ContributorId));
VerifyOwnership(c => c.Remove(command.ContributorId));
RaiseEvent(SimpleMapper.Map(command, new AppContributorRemoved()));
return this;
}
public AppDomainObject ConfigureLanguages(ConfigureLanguages command)
{
Guard.Valid(command, nameof(command), () => "Cannot remove contributor");
VerifyCreated();
RaiseEvent(SimpleMapper.Map(command, new AppLanguagesConfigured()));
return this;
}
public AppDomainObject RevokeClientKey(RevokeClientKey command)
{
Guard.Valid(command, nameof(command), () => "Cannot revoke client key");
VerifyCreated();
VerifyClientKeyFound(command);
RaiseEvent(SimpleMapper.Map(command, new AppClientKeyRevoked()));
return this;
}
public AppDomainObject CreateClientKey(CreateClientKey command)
{
Guard.Valid(command, nameof(command), () => "Cannot create client key");
VerifyCreated();
RaiseEvent(SimpleMapper.Map(command, new AppClientKeyCreated()));
return this;
}
private static AppLanguagesConfigured CreateInitialLanguage()
{
return new AppLanguagesConfigured { Languages = DefaultLanguages };
}
private static AppContributorAssigned CreateInitialOwner(ISubjectCommand command)
{
return new AppContributorAssigned { ContributorId = command.SubjectId, Permission = PermissionLevel.Owner };
}
private void VerifyCreated()
{
if (string.IsNullOrWhiteSpace(name))
@ -113,6 +173,16 @@ namespace Squidex.Write.Apps
}
}
private void VerifyClientKeyFound(RevokeClientKey command)
{
if (!clientKeys.Contains(command.ClientKey))
{
var error = new ValidationError("Client key is not part of the app", "ClientKey");
throw new ValidationException("Cannot revoke client key", error);
}
}
private void VerifyContributorFound(RemoveContributor command)
{
if (!contributors.ContainsKey(command.ContributorId))
@ -123,7 +193,7 @@ namespace Squidex.Write.Apps
}
}
private void VerifyHasStillOwner(Action<Dictionary<string, PermissionLevel>> change)
private void VerifyOwnership(Action<Dictionary<string, PermissionLevel>> change)
{
var contributorsCopy = new Dictionary<string, PermissionLevel>(contributors);

33
src/Squidex.Write/Apps/ClientKeyGenerator.cs

@ -0,0 +1,33 @@
// =========================================================================
// ClientKeyGenerator.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Security.Cryptography;
using System.Text;
namespace Squidex.Write.Apps
{
public class ClientKeyGenerator
{
public virtual string GenerateKey()
{
return Sha256(Guid.NewGuid().ToString());
}
private static string Sha256(string input)
{
using (var sha = SHA256.Create())
{
var bytes = Encoding.UTF8.GetBytes(input);
var hash = sha.ComputeHash(bytes);
return Convert.ToBase64String(hash);
}
}
}
}

26
src/Squidex.Write/Apps/Commands/ConfigureLanguages.cs

@ -0,0 +1,26 @@
// ==========================================================================
// ConfigureLanguages.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using Squidex.Infrastructure;
namespace Squidex.Write.Apps.Commands
{
public sealed class ConfigureLanguages : AppAggregateCommand, IValidatable
{
public List<Language> Languages { get; set; }
public void Validate(IList<ValidationError> errors)
{
if (Languages == null || Languages.Count == 0)
{
errors.Add(new ValidationError("Languages need at least one element.", nameof(Languages)));
}
}
}
}

30
src/Squidex.Write/Apps/Commands/CreateClientKey.cs

@ -0,0 +1,30 @@
// ==========================================================================
// CreateClientKey.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Commands;
namespace Squidex.Write.Apps.Commands
{
public sealed class CreateClientKey : AppAggregateCommand, ITimestampCommand, IValidatable
{
public string ClientKey { get; set; }
public DateTime Timestamp { get; set; }
public void Validate(IList<ValidationError> errors)
{
if (string.IsNullOrWhiteSpace(ClientKey))
{
errors.Add(new ValidationError("Client key is not assigned", nameof(ClientKey)));
}
}
}
}

26
src/Squidex.Write/Apps/Commands/RevokeClientKey.cs

@ -0,0 +1,26 @@
// ==========================================================================
// RevokeClientKey.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using Squidex.Infrastructure;
namespace Squidex.Write.Apps.Commands
{
public class RevokeClientKey : AppAggregateCommand, IValidatable
{
public string ClientKey { get; set; }
public void Validate(IList<ValidationError> errors)
{
if (string.IsNullOrWhiteSpace(ClientKey))
{
errors.Add(new ValidationError("Client key is not assigned", nameof(ClientKey)));
}
}
}
}

1
src/Squidex/Configurations/Domain/Serializers.cs

@ -24,6 +24,7 @@ namespace Squidex.Configurations.Domain
{
settings.SerializationBinder = new TypeNameSerializationBinder();
settings.ContractResolver = new CamelCasePropertyNamesContractResolver();
settings.Converters.Add(new LanguageConverter());
settings.Converters.Add(new PropertiesBagConverter());
settings.NullValueHandling = NullValueHandling.Ignore;
settings.DateFormatHandling = DateFormatHandling.IsoDateFormat;

8
src/Squidex/Configurations/Domain/WriteModule.cs

@ -30,10 +30,18 @@ namespace Squidex.Configurations.Domain
.As<ICommandHandler>()
.SingleInstance();
builder.RegisterType<EnrichWithTimestampHandler>()
.As<ICommandHandler>()
.SingleInstance();
builder.RegisterType<AppCommandHandler>()
.As<ICommandHandler>()
.SingleInstance();
builder.RegisterType<ClientKeyGenerator>()
.AsSelf()
.InstancePerDependency();
builder.RegisterType<AppDomainObject>()
.AsSelf()
.InstancePerDependency();

58
src/Squidex/Configurations/Identity/LazyClientStore.cs

@ -8,36 +8,78 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using IdentityServer4.Models;
using IdentityServer4.Stores;
using Microsoft.Extensions.Options;
using Squidex.Infrastructure;
using Squidex.Read.Apps;
using Squidex.Read.Apps.Services;
namespace Squidex.Configurations.Identity
{
public class LazyClientStore : IClientStore
{
private readonly Dictionary<string, Client> clients = new Dictionary<string, Client>(StringComparer.OrdinalIgnoreCase);
private readonly IAppProvider appProvider;
private readonly Dictionary<string, Client> staticClients = new Dictionary<string, Client>(StringComparer.OrdinalIgnoreCase);
public LazyClientStore(IOptions<MyIdentityOptions> identityOptions)
public LazyClientStore(IOptions<MyIdentityOptions> identityOptions, IAppProvider appProvider)
{
Guard.NotNull(identityOptions, nameof(identityOptions));
Guard.NotNull(appProvider, nameof(appProvider));
foreach (var client in CreateClients(identityOptions.Value))
this.appProvider = appProvider;
CreateStaticClients(identityOptions);
}
public async Task<Client> FindClientByIdAsync(string clientId)
{
var client = staticClients.GetOrDefault(clientId);
if (client == null)
{
clients[client.ClientId] = client;
return null;
}
var app = await appProvider.FindAppByNameAsync(clientId);
if (app != null)
{
client = CreateClientFromApp(app);
}
return client;
}
public Task<Client> FindClientByIdAsync(string clientId)
private void CreateStaticClients(IOptions<MyIdentityOptions> identityOptions)
{
var client = clients.GetOrDefault(clientId);
foreach (var client in CreateStaticClients(identityOptions.Value))
{
staticClients[client.ClientId] = client;
}
}
return Task.FromResult(client);
private static Client CreateClientFromApp(IAppEntity app)
{
var id = app.Name;
return new Client
{
ClientId = id,
ClientName = id,
ClientSecrets = app.ClientKeys.Select(x => new Secret(x.ClientKey, x.ExpiresUtc)).ToList(),
AccessTokenLifetime = (int)TimeSpan.FromDays(30).TotalSeconds,
AllowedGrantTypes = GrantTypes.ClientCredentials,
AllowedScopes = new List<string>
{
Constants.ApiScope
}
};
}
private static IEnumerable<Client> CreateClients(MyIdentityOptions options)
private static IEnumerable<Client> CreateStaticClients(MyIdentityOptions options)
{
const string id = Constants.FrontendClient;

65
src/Squidex/Modules/Api/Apps/AppClientKeysController.cs

@ -0,0 +1,65 @@
// ==========================================================================
// AppClientKeysController.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Infrastructure.Reflection;
using Squidex.Modules.Api.Apps.Models;
using Squidex.Pipeline;
using Squidex.Read.Apps.Services;
using Squidex.Write.Apps;
using Squidex.Write.Apps.Commands;
namespace Squidex.Modules.Api.Apps
{
[Authorize(Roles = "app-owner")]
[ApiExceptionFilter]
[ServiceFilter(typeof(AppFilterAttribute))]
public class AppClientKeysController : ControllerBase
{
private readonly IAppProvider appProvider;
private readonly ClientKeyGenerator keyGenerator;
public AppClientKeysController(ICommandBus commandBus, IAppProvider appProvider, ClientKeyGenerator keyGenerator)
: base(commandBus)
{
this.appProvider = appProvider;
this.keyGenerator = keyGenerator;
}
[HttpGet]
[Route("apps/{app}/client-keys/")]
public async Task<IActionResult> GetContributors(string app)
{
var entity = await appProvider.FindAppByNameAsync(app);
if (entity == null)
{
return NotFound();
}
var model = entity.ClientKeys.Select(x => SimpleMapper.Map(x, new ClientKeyDto())).ToList();
return Ok(model);
}
[HttpPost]
[Route("apps/{app}/client-keys/")]
public async Task<IActionResult> PostClientKey()
{
var clientKey = keyGenerator.GenerateKey();
await CommandBus.PublishAsync(new CreateClientKey { ClientKey = clientKey });
return Ok(new ClientKeyCreatedDto { ClientKey = clientKey });
}
}
}

5
src/Squidex/Modules/Api/Apps/AppContributorsController.cs

@ -15,11 +15,12 @@ using Squidex.Infrastructure.Reflection;
using Squidex.Modules.Api.Apps.Models;
using Squidex.Pipeline;
using Squidex.Read.Apps.Services;
using Squidex.Write.Apps;
using Squidex.Write.Apps.Commands;
namespace Squidex.Modules.Api.Apps
{
[Authorize]
[Authorize(Roles = "app-owner")]
[ApiExceptionFilter]
[ServiceFilter(typeof(AppFilterAttribute))]
public class AppContributorsController : ControllerBase
@ -43,7 +44,7 @@ namespace Squidex.Modules.Api.Apps
return NotFound();
}
var model = entity.Contributors.Select(x => SimpleMapper.Map(x, new AppContributorDto())).ToList();
var model = entity.Contributors.Select(x => SimpleMapper.Map(x, new ContributorDto())).ToList();
return Ok(model);
}

60
src/Squidex/Modules/Api/Apps/AppLanguagesController.cs

@ -0,0 +1,60 @@
// ==========================================================================
// AppLanguagesController.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Infrastructure.Reflection;
using Squidex.Modules.Api.Apps.Models;
using Squidex.Pipeline;
using Squidex.Read.Apps.Services;
using Squidex.Write.Apps.Commands;
namespace Squidex.Modules.Api.Apps
{
[Authorize(Roles = "app-owner")]
[ApiExceptionFilter]
[ServiceFilter(typeof(AppFilterAttribute))]
public class AppLanguagesController : ControllerBase
{
private readonly IAppProvider appProvider;
public AppLanguagesController(ICommandBus commandBus, IAppProvider appProvider)
: base(commandBus)
{
this.appProvider = appProvider;
}
[HttpGet]
[Route("apps/{app}/languages/")]
public async Task<IActionResult> GetContributors(string app)
{
var entity = await appProvider.FindAppByNameAsync(app);
if (entity == null)
{
return NotFound();
}
var model = entity.Languages.Select(x => SimpleMapper.Map(x, new LanguageDto())).ToList();
return Ok(model);
}
[HttpPost]
[Route("apps/{app}/languages/")]
public async Task<IActionResult> PostLanguages([FromBody] ConfigureLanguagesDto model)
{
await CommandBus.PublishAsync(SimpleMapper.Map(model, new ConfigureLanguages()));
return Ok();
}
}
}

2
src/Squidex/Modules/Api/Apps/Models/AssignContributorDto.cs

@ -1,5 +1,5 @@
// ==========================================================================
// PutContributorDto.cs
// AssignContributorDto.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group

15
src/Squidex/Modules/Api/Apps/Models/ClientKeyCreatedDto.cs

@ -0,0 +1,15 @@
// ==========================================================================
// ClientKeyCreatedDto.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Modules.Api.Apps.Models
{
public sealed class ClientKeyCreatedDto
{
public string ClientKey { get; set; }
}
}

19
src/Squidex/Modules/Api/Apps/Models/ClientKeyDto.cs

@ -0,0 +1,19 @@
// ==========================================================================
// ClientKeyDto.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
namespace Squidex.Modules.Api.Apps.Models
{
public sealed class ClientKeyDto
{
public string ClientKey { get; set; }
public DateTime ExpiresUtc { get; set; }
}
}

18
src/Squidex/Modules/Api/Apps/Models/ConfigureLanguagesDto.cs

@ -0,0 +1,18 @@
// ==========================================================================
// ConfigureLanguagesDto.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using Squidex.Infrastructure;
namespace Squidex.Modules.Api.Apps.Models
{
public class ConfigureLanguagesDto
{
public List<Language> Languages { get; set; }
}
}

6
src/Squidex/Modules/Api/Apps/Models/AppContributorDto.cs → src/Squidex/Modules/Api/Apps/Models/ContributorDto.cs

@ -1,5 +1,5 @@
// =========================================================================
// AppContributorDto.cs
// ==========================================================================
// ContributorDto.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
@ -12,7 +12,7 @@ using Squidex.Core.Apps;
namespace Squidex.Modules.Api.Apps.Models
{
public sealed class AppContributorDto
public sealed class ContributorDto
{
public string ContributorId { get; set; }

1
src/Squidex/Modules/Api/Apps/Models/CreateAppDto.cs

@ -5,6 +5,7 @@
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Modules.Api.Apps.Models
{
public sealed class CreateAppDto

17
src/Squidex/Modules/Api/LanguageDto.cs

@ -0,0 +1,17 @@
// ==========================================================================
// LanguageDto.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Modules.Api
{
public class LanguageDto
{
public string Iso2Code { get; set; }
public string EnglishName { get; set; }
}
}

3
src/Squidex/Modules/Api/Languages/LanguagesController.cs

@ -10,6 +10,7 @@ using System.Linq;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Reflection;
using Squidex.Pipeline;
namespace Squidex.Modules.Api.Languages
@ -22,7 +23,7 @@ namespace Squidex.Modules.Api.Languages
[Route("languages/")]
public IActionResult GetLanguages()
{
var model = Language.AllLanguages.ToList();
var model = Language.AllLanguages.Select(x => SimpleMapper.Map(x, new LanguageDto())).ToList();
return Ok(model);
}

26
src/Squidex/Pipeline/AppFilterAttribute.cs

@ -6,7 +6,9 @@
// All rights reserved.
// ==========================================================================
using System;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
@ -15,7 +17,7 @@ using Squidex.Read.Apps.Services;
namespace Squidex.Pipeline
{
public sealed class AppFilterAttribute : ActionFilterAttribute
public sealed class AppFilterAttribute : Attribute, IAsyncAuthorizationFilter
{
private readonly IAppProvider appProvider;
@ -24,7 +26,7 @@ namespace Squidex.Pipeline
this.appProvider = appProvider;
}
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
var appName = context.RouteData.Values["app"]?.ToString();
@ -40,16 +42,30 @@ namespace Squidex.Pipeline
var subject = context.HttpContext.User.FindFirst(OpenIdClaims.Subject)?.Value;
if (subject == null || app.Contributors.All(x => x.ContributorId != subject))
if (subject == null)
{
context.Result = new NotFoundResult();
return;
}
var contributor = app.Contributors.FirstOrDefault(x => string.Equals(x.ContributorId, subject, StringComparison.OrdinalIgnoreCase));
if (contributor == null)
{
context.Result = new NotFoundResult();
return;
}
var roleName = $"app-{contributor.Permission.ToString().ToLowerInvariant()}";
var defaultIdentity = context.HttpContext.User.Identities.First();
defaultIdentity
.AddClaim(
new Claim(defaultIdentity.RoleClaimType, roleName));
context.HttpContext.Features.Set<IAppFeature>(new AppFeature(app));
}
await next();
}
}
}

7
src/Squidex/app/app.module.ts

@ -10,9 +10,13 @@ import * as Ng2Browser from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { DndModule } from 'ng2-dnd';
import {
ApiUrlConfig,
AppClientKeysService,
AppContributorsService,
AppLanguagesService,
AppMustExistGuard,
AppsStoreService,
AppsService,
@ -45,6 +49,7 @@ const baseUrl = window.location.protocol + '//' + window.location.host + '/';
@Ng2.NgModule({
imports: [
DndModule.forRoot(),
Ng2Browser.BrowserModule,
SqxAppModule,
SqxAuthModule,
@ -57,7 +62,9 @@ const baseUrl = window.location.protocol + '//' + window.location.host + '/';
AppComponent
],
providers: [
AppClientKeysService,
AppContributorsService,
AppLanguagesService,
AppsStoreService,
AppsService,
AppMustExistGuard,

26
src/Squidex/app/components/internal/app/settings/credentials-page.component.html

@ -9,6 +9,32 @@
</h1>
</div>
<div class="layout-middle-content">
<div class="card">
<div class="card-block">
<table class="table table-borderless table-fixed">
<colgroup>
<col style="width: 100%" />
<col style="width: 160px" />
<col style="width: 120px" />
</colgroup>
<tr *ngFor="let clientKey of appClientKeys">
<td>
<input readonly class="form-control" [ngModel]="clientKey.clientKey" />
</td>
<td>
<button class="btn btn-block btn-default">Create Token</button>
</td>
<td>
<button class="btn btn-block btn-danger">Revoke</button>
</td>
</tr>
</table>
</div>
<div class="card-footer">
<button type="submit" class="btn btn-primary" (click)="createClientKey()">Create Client Key</button>
</div>
</div>
</div>
</div>
</div>

11
src/Squidex/app/components/internal/app/settings/credentials-page.component.scss

@ -3,4 +3,15 @@
.layout-title-icon {
color: $color-section-settings;
}
.card {
& {
max-width: 700px;
}
&-block {
padding-left: .5rem;
padding-right: .5rem;
}
}

25
src/Squidex/app/components/internal/app/settings/credentials-page.component.ts

@ -7,7 +7,12 @@
import * as Ng2 from '@angular/core';
import { AppsStoreService, TitleService } from 'shared';
import {
AppsStoreService,
AppClientKeyDto,
AppClientKeysService,
TitleService
} from 'shared';
@Ng2.Component({
selector: 'sqx-credentials-page',
@ -16,10 +21,14 @@ import { AppsStoreService, TitleService } from 'shared';
})
export class CredentialsPageComponent implements Ng2.OnInit {
private appSubscription: any | null = null;
private appName: string | null = null;
public appClientKeys: AppClientKeyDto[] = [];
constructor(
private readonly titles: TitleService,
private readonly appsStore: AppsStoreService
private readonly appsStore: AppsStoreService,
private readonly appClientKeysService: AppClientKeysService
) {
}
@ -27,7 +36,13 @@ export class CredentialsPageComponent implements Ng2.OnInit {
this.appSubscription =
this.appsStore.selectedApp.subscribe(app => {
if (app) {
this.appName = app.name;
this.titles.setTitle('{appName} | Settings | Credentials', { appName: app.name });
this.appClientKeysService.getClientKeys(app.name).subscribe(clientKeys => {
this.appClientKeys = clientKeys;
});
}
});
}
@ -35,5 +50,11 @@ export class CredentialsPageComponent implements Ng2.OnInit {
public ngOnDestroy() {
this.appSubscription.unsubscribe();
}
public createClientKey() {
this.appClientKeysService.postClientKey(this.appName).subscribe(clientKey => {
this.appClientKeys.push(clientKey);
})
}
}

40
src/Squidex/app/components/internal/app/settings/languages-page.component.html

@ -9,6 +9,46 @@
</h1>
</div>
<div class="layout-middle-content">
<div class="card">
<div class="card-block">
<table class="table table-borderless table-fixed" dnd-sortable-container [sortableData]="appLanguages">
<colgroup>
<col style="width: 60px" />
<col style="width: 100%" />
<col style="width: 110px" />
</colgroup>
<tr *ngFor="let language of appLanguages; let i = index" dnd-sortable [sortableIndex]="i">
<td>
<span class="language-code">
{{language.iso2Code}}
</span>
</td>
<td>
<span class="language-name">
{{language.englishName}}
</span>
</td>
<td>
<button class="btn btn-block btn-sm btn-danger" [disabled]="i === 0" (click)="removeLanguage(language)">Remove</button>
</td>
</tr>
</table>
<button class="btn btn-primary" (click)="saveLanguages()" [disabled]="isSaving">{{isSaving ? 'Saving...' : 'Save'}}</button>
</div>
<div class="card-footer">
<form class="form-inline" (submit)="addLanguage()" name="addLanguageForm">
<div class="form-group">
<select class="form-control language-select" [(ngModel)]="selectedLanguage" name="newLanguage">
<option *ngFor="let language of allLanguages" [ngValue]="language">{{language.englishName}}</option>
</select>
</div>
<button type="submit" class="btn btn-primary" [disabled]="!selectedLanguage">Add</button>
</form>
</div>
</div>
</div>
</div>
</div>

14
src/Squidex/app/components/internal/app/settings/languages-page.component.scss

@ -3,4 +3,18 @@
.layout-title-icon {
color: $color-section-settings;
}
.card {
max-width: 700px;
}
.language {
&-select {
max-width: 200px;
}
&-name {
@include truncate;
}
}

54
src/Squidex/app/components/internal/app/settings/languages-page.component.ts

@ -7,7 +7,13 @@
import * as Ng2 from '@angular/core';
import { AppsStoreService, TitleService } from 'shared';
import {
AppLanguagesService,
AppsStoreService,
LanguageDto,
LanguageService,
TitleService
} from 'shared';
@Ng2.Component({
selector: 'sqx-languages-page',
@ -16,18 +22,41 @@ import { AppsStoreService, TitleService } from 'shared';
})
export class LanguagesPageComponent implements Ng2.OnInit {
private appSubscription: any | null = null;
private appName: string;
public allLanguages: LanguageDto[] = null;
public appLanguages: LanguageDto[] = [];
public selectedLanguage: LanguageDto | null = null;
public isSaving: boolean;
public get newLanguages() {
return this.allLanguages.filter(x => !this.appLanguages.find(l => l.iso2Code === x.iso2Code));
}
constructor(
private readonly titles: TitleService,
private readonly appsStore: AppsStoreService
private readonly appsStore: AppsStoreService,
private readonly appLanguagesService: AppLanguagesService,
private readonly languagesService: LanguageService
) {
}
public ngOnInit() {
this.languagesService.getLanguages().subscribe(languages => {
this.allLanguages = languages;
});
this.appSubscription =
this.appsStore.selectedApp.subscribe(app => {
if (app) {
this.appName = app.name;
this.titles.setTitle('{appName} | Settings | Languages', { appName: app.name });
this.appLanguagesService.getLanguages(app.name).subscribe(appLanguages => {
this.appLanguages = appLanguages;
});
}
});
}
@ -35,5 +64,26 @@ export class LanguagesPageComponent implements Ng2.OnInit {
public ngOnDestroy() {
this.appSubscription.unsubscribe();
}
public removeLanguage(language: LanguageDto) {
this.appLanguages.splice(this.appLanguages.indexOf(language), 1);
}
public addLanguage() {
this.appLanguages.push(this.selectedLanguage);
this.selectedLanguage = null;
}
public saveLanguages() {
this.isSaving = true;
this.appLanguagesService.postLanguages(this.appName, this.appLanguages.map(l => l.iso2Code))
.delay(500)
.finally(() => {
this.isSaving = false;
})
.subscribe();
}
}

3
src/Squidex/app/components/internal/module.ts

@ -9,6 +9,8 @@ import * as Ng2 from '@angular/core';
import { Ng2CompleterModule } from 'ng2-completer';
import { DndModule } from 'ng2-dnd';
import { SqxFrameworkModule } from 'shared';
import { SqxLayoutModule } from 'components/layout';
@ -26,6 +28,7 @@ import {
@Ng2.NgModule({
imports: [
DndModule,
Ng2CompleterModule,
SqxFrameworkModule,
SqxLayoutModule

2
src/Squidex/app/shared/index.ts

@ -9,6 +9,8 @@ export * from './guards/app-must-exist.guard';
export * from './guards/must-be-authenticated.guard';
export * from './guards/must-be-not-authenticated.guard';
export * from './services/app-contributors.service';
export * from './services/app-client-keys.service';
export * from './services/app-languages.service';
export * from './services/apps-store.service';
export * from './services/apps.service';
export * from './services/auth.service';

52
src/Squidex/app/shared/services/app-client-keys.service.ts

@ -0,0 +1,52 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import * as Ng2 from '@angular/core';
import * as Ng2Http from '@angular/http';
import { Observable } from 'rxjs';
import { ApiUrlConfig, DateTime } from 'framework';
import { AuthService } from './auth.service';
export class AppClientKeyDto {
constructor(
public readonly clientKey: string,
public readonly expiresUtc: DateTime
) {
}
}
@Ng2.Injectable()
export class AppClientKeysService {
constructor(
private readonly authService: AuthService,
private readonly apiUrl: ApiUrlConfig,
private readonly http: Ng2Http.Http
) {
}
public getClientKeys(appName: string): Observable<AppClientKeyDto[]> {
return this.authService.authGet(this.apiUrl.buildUrl(`api/apps/${appName}/client-keys`))
.map(response => {
const body: any[] = response.json();
return body.map(item => {
return new AppClientKeyDto(item.clientKey, DateTime.parseISO_UTC(item.expiresUtc));
});
});
}
public postClientKey(appName: string): Observable<AppClientKeyDto> {
return this.authService.authPost(this.apiUrl.buildUrl(`api/apps/${appName}/client-keys`), {})
.map(response => {
const body = response.json();
return new AppClientKeyDto(body.clientKey, DateTime.now().addYears(1));
});
}
}

1
src/Squidex/app/shared/services/app-contributors.service.ts

@ -10,7 +10,6 @@ import * as Ng2 from '@angular/core';
import { Observable } from 'rxjs';
import { ApiUrlConfig } from 'framework';
import { AuthService } from './auth.service';
export class AppContributorDto {

76
src/Squidex/app/shared/services/app-languages.service.spec.ts

@ -0,0 +1,76 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import * as TypeMoq from 'typemoq';
import * as Ng2Http from '@angular/http';
import { Observable } from 'rxjs';
import {
ApiUrlConfig,
AppLanguagesService,
AuthService,
LanguageDto,
} from './../';
describe('AppLanguagesService', () => {
let authService: TypeMoq.Mock<AuthService>;
let appLanguagesService: AppLanguagesService;
beforeEach(() => {
authService = TypeMoq.Mock.ofType(AuthService);
appLanguagesService = new AppLanguagesService(authService.object, new ApiUrlConfig('http://service/p/'));
});
it('should make get request with auth service to get app languages', () => {
authService.setup(x => x.authGet('http://service/p/api/apps/my-app/languages'))
.returns(() => Observable.of(
new Ng2Http.Response(
new Ng2Http.ResponseOptions({
body: [{
iso2Code: 'de',
englishName: 'German'
}, {
iso2Code: 'en',
englishName: 'English'
}]
})
)
))
.verifiable(TypeMoq.Times.once());
let languages: LanguageDto[] = null;
appLanguagesService.getLanguages('my-app').subscribe(result => {
languages = result;
}).unsubscribe();
expect(languages).toEqual(
[
new LanguageDto('de', 'German'),
new LanguageDto('en', 'English'),
]);
authService.verifyAll();
});
it('should make post request to configure languages', () => {
const languages = ['de', 'en'];
authService.setup(x => x.authPost('http://service/p/api/apps/my-app/languages', TypeMoq.It.is(y => y['languages'] === languages)))
.returns(() => Observable.of(
new Ng2Http.Response(
new Ng2Http.ResponseOptions()
)
))
.verifiable(TypeMoq.Times.once());
appLanguagesService.postLanguages('my-app', languages);
authService.verifyAll();
});
});

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

@ -0,0 +1,40 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import * as Ng2 from '@angular/core';
import { Observable } from 'rxjs';
import { ApiUrlConfig } from 'framework';
import { AuthService } from './auth.service';
import { LanguageDto } from './languages.service';
@Ng2.Injectable()
export class AppLanguagesService {
constructor(
private readonly authService: AuthService,
private readonly apiUrl: ApiUrlConfig
) {
}
public getLanguages(appName: string): Observable<LanguageDto[]> {
return this.authService.authGet(this.apiUrl.buildUrl(`api/apps/${appName}/languages`))
.map(response => {
const body: any[] = response.json();
return body.map(item => {
return new LanguageDto(
item.iso2Code,
item.englishName);
});
});
}
public postLanguages(appName: string, languageCodes: string[]): Observable<any> {
return this.authService.authPost(this.apiUrl.buildUrl(`api/apps/${appName}/languages`), { languages: languageCodes });
}
}

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

@ -9,7 +9,6 @@ import * as Ng2 from '@angular/core';
import { Observable } from 'rxjs';
import { ApiUrlConfig, DateTime } from 'framework';
import { AuthService } from './auth.service';
export class AppDto {

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

@ -9,7 +9,6 @@ import * as Ng2 from '@angular/core';
import { Observable } from 'rxjs';
import { ApiUrlConfig } from 'framework';
import { AuthService } from './auth.service';
export class LanguageDto {

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

@ -10,7 +10,6 @@ import * as Ng2 from '@angular/core';
import { Observable } from 'rxjs';
import { ApiUrlConfig } from 'framework';
import { AuthService } from './auth.service';
export class UserDto {

1
src/Squidex/app/theme/_layout.scss

@ -70,6 +70,7 @@ h1 {
&-content {
@include flex-grow(1);
padding: $padding-layout-v;
overflow-y: scroll;
}
}
}

0
src/Squidex/app/theme/_completer.scss → src/Squidex/app/theme/_lib-completer.scss

24
src/Squidex/app/theme/_lib-dnd.scss

@ -0,0 +1,24 @@
@import '_mixins';
@import '_vars';
$color-drag-border: #000;
.dnd {
&-drag {
&-start,
&-enter {
@include opacity(.6);
}
&-start,
&-over,
&-enter {
border: 2px dashed $color-drag-border;
}
}
&-sortable-drag {
@include opacity(.6);
border: 2px dashed $color-drag-border;
}
}

3
src/Squidex/app/theme/theme.scss

@ -1,3 +1,4 @@
@import '_bootstrap.scss';
@import '_layout.scss';
@import '_completer.scss';
@import '_lib-completer.scss';
@import '_lib-dnd.scss';

1
src/Squidex/package.json

@ -30,6 +30,7 @@
"moment": "^2.14.0",
"mousetrap": "^1.6.0",
"ng2-completer": "^0.2.3",
"ng2-dnd": "^2.0.1",
"oidc-client": "^1.2.1-beta.3",
"reflect-metadata": "^0.1.3",
"rxjs": "5.0.0-beta.12",

51
tests/Squidex.Infrastructure.Tests/CQRS/Commands/EnrichWithTimestampHandlerTests.cs

@ -0,0 +1,51 @@
// ==========================================================================
// EnrichWithTimestampHandlerTests.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Xunit;
namespace Squidex.Infrastructure.CQRS.Commands
{
public sealed class EnrichWithTimestampHandlerTests
{
private sealed class NormalCommand : AggregateCommand
{
}
private sealed class TimestampCommand : AggregateCommand, ITimestampCommand
{
public DateTime Timestamp { get; set; }
}
[Fact]
public async Task Should_set_timestamp_when_is_timestamp_command()
{
var utc = DateTime.Today;
var sut = new EnrichWithTimestampHandler(() => utc);
var command = new TimestampCommand();
var result = await sut.HandleAsync(new CommandContext(command));
Assert.False(result);
Assert.Equal(utc, command.Timestamp);
}
[Fact]
public async Task Should_do_nothing_for_normal_command()
{
var utc = DateTime.Today;
var sut = new EnrichWithTimestampHandler(() => utc);
var result = await sut.HandleAsync(new CommandContext(new NormalCommand()));
Assert.False(result);
}
}
}

29
tests/Squidex.Infrastructure.Tests/LanguageTests.cs

@ -8,12 +8,21 @@
using System;
using System.Linq;
using Newtonsoft.Json;
using Squidex.Infrastructure.Json;
using Xunit;
namespace Squidex.Infrastructure
{
public class LanguageTests
{
private static readonly JsonSerializerSettings serializerSettings = new JsonSerializerSettings();
static LanguageTests()
{
serializerSettings.Converters.Add(new LanguageConverter());
}
[Theory]
[InlineData("")]
[InlineData(" ")]
@ -40,6 +49,26 @@ namespace Squidex.Infrastructure
Assert.True(Language.AllLanguages.Count() > 100);
}
[Fact]
public void Should_serialize_and_deserialize_null_language()
{
var input = Tuple.Create<Language>(null);
var json = JsonConvert.SerializeObject(input, serializerSettings);
var output = JsonConvert.DeserializeObject<Tuple<Language>>(json, serializerSettings);
Assert.Equal(output.Item1, input.Item1);
}
[Fact]
public void Should_serialize_and_deserialize_valid_language()
{
var input = Tuple.Create(Language.GetLanguage("de"));
var json = JsonConvert.SerializeObject(input, serializerSettings);
var output = JsonConvert.DeserializeObject<Tuple<Language>>(json, serializerSettings);
Assert.Equal(output.Item1, input.Item1);
}
[Theory]
[InlineData("de", "German")]
[InlineData("en", "English")]

71
tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs

@ -6,6 +6,7 @@
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using System.Threading.Tasks;
using Moq;
using Squidex.Infrastructure;
@ -26,9 +27,12 @@ namespace Squidex.Write.Tests.Apps
private readonly Mock<IAppRepository> appRepository = new Mock<IAppRepository>();
private readonly Mock<IUserRepository> userRepository = new Mock<IUserRepository>();
private readonly AppCommandHandler sut;
private readonly AppDomainObject app;
public AppCommandHandlerTests()
{
app = new AppDomainObject(Id, 0);
sut = new AppCommandHandler(
DomainObjectFactory.Object,
DomainObjectRepository.Object,
@ -41,7 +45,7 @@ namespace Squidex.Write.Tests.Apps
{
appRepository.Setup(x => x.FindAppByNameAsync("my-app")).Returns(Task.FromResult(new Mock<IAppEntity>().Object)).Verifiable();
await TestCreate(new AppDomainObject(Id, 0), async _ =>
await TestCreate(app, async _ =>
{
await Assert.ThrowsAsync<ValidationException>(async () => await sut.On(new CreateApp { Name = "my-app" }));
}, false);
@ -56,20 +60,31 @@ namespace Squidex.Write.Tests.Apps
appRepository.Setup(x => x.FindAppByNameAsync("my-app")).Returns(Task.FromResult<IAppEntity>(null)).Verifiable();
await TestCreate(new AppDomainObject(Id, 0), async _ =>
await TestCreate(app, async _ =>
{
await sut.On(command);
});
appRepository.VerifyAll();
}
[Fact]
public async Task ConfigureLanguages_should_update_domain_object()
{
CreateApp();
var command = new ConfigureLanguages { AggregateId = Id, Languages = new List<Language> { Language.GetLanguage("de") } };
await TestUpdate(app, async _ =>
{
await sut.On(command);
});
}
[Fact]
public async Task AssignContributor_should_throw_if_user_not_found()
{
var app =
new AppDomainObject(Id, 0)
.Create(new CreateApp { Name = "my-app", SubjectId = "123" });
CreateApp();
var command = new AssignContributor { AggregateId = Id, ContributorId = "456" };
@ -84,9 +99,7 @@ namespace Squidex.Write.Tests.Apps
[Fact]
public async Task AssignContributor_should_assign_if_user_found()
{
var app =
new AppDomainObject(Id, 0)
.Create(new CreateApp { Name = "my-app", SubjectId = "123" });
CreateApp();
var command = new AssignContributor { AggregateId = Id, ContributorId = "456" };
@ -101,10 +114,8 @@ namespace Squidex.Write.Tests.Apps
[Fact]
public async Task RemoveContributor_should_update_domain_object()
{
var app =
new AppDomainObject(Id, 0)
.Create(new CreateApp { Name = "my-app", SubjectId = "123" })
.AssignContributor(new AssignContributor { ContributorId = "456" });
CreateApp()
.AssignContributor(new AssignContributor { ContributorId = "456" });
var command = new RemoveContributor { AggregateId = Id, ContributorId = "456" };
@ -113,5 +124,39 @@ namespace Squidex.Write.Tests.Apps
await sut.On(command);
});
}
[Fact]
public async Task CreateClientKey_should_update_domain_object()
{
CreateApp();
var command = new CreateClientKey { AggregateId = Id, ClientKey = "456" };
await TestUpdate(app, async _ =>
{
await sut.On(command);
});
}
[Fact]
public async Task RevokeClientKey_should_update_domain_object()
{
CreateApp()
.CreateClientKey(new CreateClientKey { ClientKey = "456" });
var command = new RevokeClientKey { AggregateId = Id, ClientKey = "456" };
await TestUpdate(app, async _ =>
{
await sut.On(command);
});
}
private AppDomainObject CreateApp()
{
app.Create(new CreateApp { Name = "my-app", SubjectId = "123" });
return app;
}
}
}

149
tests/Squidex.Write.Tests/Apps/AppDomainObjectTests.cs

@ -7,11 +7,13 @@
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using FluentAssertions;
using Squidex.Core.Apps;
using Squidex.Events.Apps;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Write.Apps;
using Squidex.Write.Apps.Commands;
@ -22,14 +24,21 @@ namespace Squidex.Write.Tests.Apps
public class AppDomainObjectTests
{
private const string TestName = "app";
private readonly AppDomainObject sut = new AppDomainObject(Guid.NewGuid(), 0);
private readonly AppDomainObject sut;
private readonly string subjectId = Guid.NewGuid().ToString();
private readonly string contributorId = Guid.NewGuid().ToString();
private readonly string clientKey = Guid.NewGuid().ToString();
private readonly List<Language> languages = new List<Language> { Language.GetLanguage("de") };
public AppDomainObjectTests()
{
sut = new AppDomainObject(Guid.NewGuid(), 0);
}
[Fact]
public void Create_should_throw_if_created()
{
sut.Create(new CreateApp { Name = TestName, SubjectId = subjectId });
CreateApp();
Assert.Throws<DomainException>(() => sut.Create(new CreateApp { Name = TestName }));
}
@ -41,7 +50,7 @@ namespace Squidex.Write.Tests.Apps
}
[Fact]
public void Create_should_specify_and_owner()
public void Create_should_specify_name_and_owner()
{
sut.Create(new CreateApp { Name = TestName, SubjectId = subjectId });
@ -53,7 +62,8 @@ namespace Squidex.Write.Tests.Apps
new IEvent[]
{
new AppCreated { Name = TestName },
new AppContributorAssigned { ContributorId = subjectId, Permission = PermissionLevel.Owner }
new AppContributorAssigned { ContributorId = subjectId, Permission = PermissionLevel.Owner },
new AppLanguagesConfigured { Languages= new List<Language> { Language.GetLanguage("de") } }
});
}
@ -66,7 +76,7 @@ namespace Squidex.Write.Tests.Apps
[Fact]
public void AssignContributor_should_throw_if_single_owner_becomes_non_owner()
{
sut.Create(new CreateApp { Name = TestName, SubjectId = subjectId });
CreateApp();
Assert.Throws<ValidationException>(() => sut.AssignContributor(new AssignContributor { ContributorId = subjectId, Permission = PermissionLevel.Editor }));
}
@ -74,12 +84,13 @@ namespace Squidex.Write.Tests.Apps
[Fact]
public void AssignContributor_should_create_events()
{
sut.Create(new CreateApp { Name = TestName, SubjectId = subjectId });
CreateApp();
sut.AssignContributor(new AssignContributor { ContributorId = contributorId, Permission = PermissionLevel.Editor });
Assert.Equal(PermissionLevel.Editor, sut.Contributors[contributorId]);
sut.GetUncomittedEvents().Select(x => x.Payload).Skip(2).ToArray()
sut.GetUncomittedEvents().Select(x => x.Payload).ToArray()
.ShouldBeEquivalentTo(
new IEvent[]
{
@ -96,7 +107,7 @@ namespace Squidex.Write.Tests.Apps
[Fact]
public void RemoveContributor_should_throw_if_all_owners_removed()
{
sut.Create(new CreateApp { Name = TestName, SubjectId = subjectId });
CreateApp();
Assert.Throws<ValidationException>(() => sut.RemoveContributor(new RemoveContributor { ContributorId = subjectId }));
}
@ -104,27 +115,141 @@ namespace Squidex.Write.Tests.Apps
[Fact]
public void RemoveContributor_should_throw_if_contributor_not_found()
{
sut.Create(new CreateApp { Name = TestName, SubjectId = subjectId });
CreateApp();
sut.AssignContributor(new AssignContributor { ContributorId = contributorId, Permission = PermissionLevel.Editor });
Assert.Throws<ValidationException>(() => sut.RemoveContributor(new RemoveContributor { ContributorId = "123" }));
Assert.Throws<ValidationException>(() => sut.RemoveContributor(new RemoveContributor { ContributorId = "not-found" }));
}
[Fact]
public void RemoveContributor_should_create_events_and_remove_contributor()
{
sut.Create(new CreateApp { Name = TestName, SubjectId = subjectId });
CreateApp();
sut.AssignContributor(new AssignContributor { ContributorId = contributorId, Permission = PermissionLevel.Editor });
sut.RemoveContributor(new RemoveContributor { ContributorId = contributorId });
Assert.False(sut.Contributors.ContainsKey(contributorId));
sut.GetUncomittedEvents().Select(x => x.Payload).Skip(3).ToArray()
sut.GetUncomittedEvents().Select(x => x.Payload).Skip(1).ToArray()
.ShouldBeEquivalentTo(
new IEvent[]
{
new AppContributorRemoved { ContributorId = contributorId }
});
}
[Fact]
public void ConfigureLanguages_should_throw_if_not_created()
{
Assert.Throws<DomainException>(() => sut.ConfigureLanguages(new ConfigureLanguages { Languages = languages }));
}
[Fact]
public void ConfigureLanguages_should_throw_if_languages_are_null_or_empty()
{
CreateApp();
Assert.Throws<ValidationException>(() => sut.ConfigureLanguages(new ConfigureLanguages()));
Assert.Throws<ValidationException>(() => sut.ConfigureLanguages(new ConfigureLanguages { Languages = new List<Language>() }));
}
[Fact]
public void ConfigureLanguages_should_create_events()
{
CreateApp();
sut.ConfigureLanguages(new ConfigureLanguages { Languages = languages });
Assert.False(sut.Contributors.ContainsKey(contributorId));
sut.GetUncomittedEvents().Select(x => x.Payload).ToArray()
.ShouldBeEquivalentTo(
new IEvent[]
{
new AppLanguagesConfigured { Languages = languages }
});
}
[Fact]
public void CreateClientKey_should_throw_if_not_created()
{
Assert.Throws<DomainException>(() => sut.CreateClientKey(new CreateClientKey { ClientKey = clientKey }));
}
[Fact]
public void CreateClientKey_should_throw_if_client_key_is_null_or_empty()
{
CreateApp();
Assert.Throws<ValidationException>(() => sut.CreateClientKey(new CreateClientKey()));
Assert.Throws<ValidationException>(() => sut.CreateClientKey(new CreateClientKey { ClientKey = string.Empty }));
}
[Fact]
public void CreateClientKey_should_create_events()
{
var now = DateTime.Today;
CreateApp();
sut.CreateClientKey(new CreateClientKey { ClientKey = clientKey, Timestamp = now });
Assert.False(sut.Contributors.ContainsKey(contributorId));
sut.GetUncomittedEvents().Select(x => x.Payload).ToArray()
.ShouldBeEquivalentTo(
new IEvent[]
{
new AppClientKeyCreated { ClientKey = clientKey, ExpiresUtc = now.AddYears(1) }
});
}
[Fact]
public void RevokeClientKey_should_throw_if_not_created()
{
Assert.Throws<DomainException>(() => sut.RevokeClientKey(new RevokeClientKey { ClientKey = "not-found" }));
}
[Fact]
public void RevokeClientKey_should_throw_if_client_key_is_null_or_empty()
{
CreateApp();
Assert.Throws<ValidationException>(() => sut.RevokeClientKey(new RevokeClientKey()));
Assert.Throws<ValidationException>(() => sut.RevokeClientKey(new RevokeClientKey { ClientKey = string.Empty }));
}
[Fact]
public void RevokeClientKey_should_throw_if_key_not_found()
{
CreateApp();
Assert.Throws<ValidationException>(() => sut.RevokeClientKey(new RevokeClientKey { ClientKey = "not-found" }));
}
[Fact]
public void RevokeClientKey_should_create_events()
{
CreateApp();
sut.CreateClientKey(new CreateClientKey { ClientKey = clientKey });
sut.RevokeClientKey(new RevokeClientKey { ClientKey = clientKey });
sut.GetUncomittedEvents().Select(x => x.Payload).Skip(1).ToArray()
.ShouldBeEquivalentTo(
new IEvent[]
{
new AppClientKeyRevoked { ClientKey = clientKey }
});
}
private void CreateApp()
{
sut.Create(new CreateApp { Name = TestName, SubjectId = subjectId });
((IAggregate)sut).ClearUncommittedEvents();
}
}
}

26
tests/Squidex.Write.Tests/Apps/ClientKeyGeneratorTests.cs

@ -0,0 +1,26 @@
// =========================================================================
// ClientKeyGeneratorTests.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Squidex.Write.Apps;
using Xunit;
namespace Squidex.Write.Tests.Apps
{
public class ClientKeyGeneratorTests
{
private readonly ClientKeyGenerator sut = new ClientKeyGenerator();
[Fact]
public void Should_create_very_long_client_key()
{
var key = sut.GenerateKey();
Assert.Equal(44, key.Length);
}
}
}
Loading…
Cancel
Save