Browse Source

Migrate to shared hosting.

pull/613/head
Sebastian 5 years ago
parent
commit
bcaaf89ca0
  1. 3
      backend/extensions/Squidex.Extensions/Samples/AssetStore/MemoryAssetStorePlugin.cs
  2. 4
      backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj
  3. 3
      backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidatorPlugin.cs
  4. 5
      backend/src/Migrations/MigrationPath.cs
  5. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs
  6. 4
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/DependencyInjectionExtensions.cs
  7. 4
      backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj
  8. 1
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs
  9. 20
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetStats.cs
  10. 1
      backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Elastic/ElasticSearchTextIndex.cs
  11. 18
      backend/src/Squidex.Domain.Apps.Entities/History/NotifoService.cs
  12. 6
      backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/RuleRunnerGrain.cs
  13. 2
      backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj
  14. 1
      backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore.cs
  15. 1
      backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Reader.cs
  16. 2
      backend/src/Squidex.Infrastructure.Azure/Squidex.Infrastructure.Azure.csproj
  17. 6
      backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs
  18. 5
      backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionClient.cs
  19. 1
      backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs
  20. 6
      backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs
  21. 12
      backend/src/Squidex.Infrastructure.RabbitMq/CQRS/Events/RabbitMqEventConsumer.cs
  22. 20
      backend/src/Squidex.Infrastructure/Configuration/Alternatives.cs
  23. 70
      backend/src/Squidex.Infrastructure/Configuration/ConfigurationExtensions.cs
  24. 26
      backend/src/Squidex.Infrastructure/ConfigurationException.cs
  25. 38
      backend/src/Squidex.Infrastructure/DelegateInitializer.cs
  26. 110
      backend/src/Squidex.Infrastructure/DependencyInjection/DependencyInjectionExtensions.cs
  27. 17
      backend/src/Squidex.Infrastructure/IBackgroundProcess.cs
  28. 19
      backend/src/Squidex.Infrastructure/IInitializable.cs
  29. 1
      backend/src/Squidex.Infrastructure/LanguagesInitializer.cs
  30. 8
      backend/src/Squidex.Infrastructure/Orleans/GrainBootstrap.cs
  31. 7
      backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj
  32. 24
      backend/src/Squidex.Infrastructure/UsageTracking/ApiStats.cs
  33. 29
      backend/src/Squidex.Infrastructure/UsageTracking/ApiStatsSummary.cs
  34. 19
      backend/src/Squidex.Infrastructure/UsageTracking/StoredUsage.cs
  35. 44
      backend/src/Squidex.Web/Pipeline/CleanupHostMiddleware.cs
  36. 58
      backend/src/Squidex.Web/Services/UrlGenerator.cs
  37. 159
      backend/src/Squidex.Web/UrlsOptions.cs
  38. 6
      backend/src/Squidex/Areas/Api/Config/OpenApi/CommonProcessor.cs
  39. 10
      backend/src/Squidex/Areas/Api/Config/OpenApi/SecurityProcessor.cs
  40. 12
      backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs
  41. 1
      backend/src/Squidex/Areas/Api/Controllers/Translations/Models/TranslationDto.cs
  42. 1
      backend/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs
  43. 13
      backend/src/Squidex/Areas/IdentityServer/Config/CreateAdminInitializer.cs
  44. 14
      backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs
  45. 39
      backend/src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs
  46. 9
      backend/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs
  47. 48
      backend/src/Squidex/Config/Authentication/IdentityServerServices.cs
  48. 3
      backend/src/Squidex/Config/Authentication/IdentityServices.cs
  49. 6
      backend/src/Squidex/Config/Domain/AssetServices.cs
  50. 3
      backend/src/Squidex/Config/Domain/CommandsServices.cs
  51. 3
      backend/src/Squidex/Config/Domain/ContentsServices.cs
  52. 18
      backend/src/Squidex/Config/Domain/EventPublishersServices.cs
  53. 3
      backend/src/Squidex/Config/Domain/HealthCheckServices.cs
  54. 39
      backend/src/Squidex/Config/Domain/InfrastructureServices.cs
  55. 43
      backend/src/Squidex/Config/Domain/LoggingServices.cs
  56. 3
      backend/src/Squidex/Config/Domain/MigrationServices.cs
  57. 3
      backend/src/Squidex/Config/Domain/NotificationsServices.cs
  58. 8
      backend/src/Squidex/Config/Domain/QueryServices.cs
  59. 3
      backend/src/Squidex/Config/Domain/RuleServices.cs
  60. 3
      backend/src/Squidex/Config/Domain/SerializationInitializer.cs
  61. 6
      backend/src/Squidex/Config/Orleans/Helper.cs
  62. 38
      backend/src/Squidex/Config/Startup/BackgroundHost.cs
  63. 37
      backend/src/Squidex/Config/Startup/InitializerHost.cs
  64. 16
      backend/src/Squidex/Config/Startup/LogConfigurationHost.cs
  65. 16
      backend/src/Squidex/Config/Startup/MigrationRebuilderHost.cs
  66. 16
      backend/src/Squidex/Config/Startup/MigratorHost.cs
  67. 47
      backend/src/Squidex/Config/Startup/SafeHostedService.cs
  68. 52
      backend/src/Squidex/Config/Web/WebExtensions.cs
  69. 28
      backend/src/Squidex/Config/Web/WebServices.cs
  70. 8
      backend/src/Squidex/Program.cs
  71. 11
      backend/src/Squidex/Squidex.csproj
  72. 3
      backend/src/Squidex/Startup.cs
  73. 4
      backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientsTests.cs
  74. 75
      backend/tests/Squidex.Web.Tests/Pipeline/CleanupHostMiddlewareTests.cs
  75. 65
      backend/tests/Squidex.Web.Tests/UrlsOptionsTests.cs

3
backend/extensions/Squidex.Extensions/Samples/AssetStore/MemoryAssetStorePlugin.cs

@ -50,8 +50,7 @@ namespace Squidex.Extensions.Samples.AssetStore
{ {
services.AddSingleton<IStartupFilter>(this); services.AddSingleton<IStartupFilter>(this);
services.AddSingletonAs<MemoryAssetStore>() services.AddSingleton<IAssetStore, MemoryAssetStore>();
.As<IAssetStore>();
} }
} }
} }

4
backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj

@ -13,11 +13,11 @@
<PackageReference Include="Confluent.Kafka" Version="1.5.3" /> <PackageReference Include="Confluent.Kafka" Version="1.5.3" />
<PackageReference Include="Confluent.SchemaRegistry.Serdes" Version="1.3.0" /> <PackageReference Include="Confluent.SchemaRegistry.Serdes" Version="1.3.0" />
<PackageReference Include="CoreTweet" Version="1.0.0.483" /> <PackageReference Include="CoreTweet" Version="1.0.0.483" />
<PackageReference Include="Datadog.Trace" Version="1.21.0" /> <PackageReference Include="Datadog.Trace" Version="1.21.1" />
<PackageReference Include="Elasticsearch.Net" Version="7.10.1" /> <PackageReference Include="Elasticsearch.Net" Version="7.10.1" />
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.16.0" /> <PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.16.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
<PackageReference Include="Microsoft.OData.Core" Version="7.7.3" /> <PackageReference Include="Microsoft.OData.Core" Version="7.8.1" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="NodaTime" Version="3.0.3" /> <PackageReference Include="NodaTime" Version="3.0.3" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />

3
backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidatorPlugin.cs

@ -16,8 +16,7 @@ namespace Squidex.Extensions.Validation
{ {
public void ConfigureServices(IServiceCollection services, IConfiguration config) public void ConfigureServices(IServiceCollection services, IConfiguration config)
{ {
services.AddSingletonAs<CompositeUniqueValidatorFactory>() services.AddSingleton<IValidatorsFactory, CompositeUniqueValidatorFactory>();
.As<IValidatorsFactory>();
} }
} }
} }

5
backend/src/Migrations/MigrationPath.cs

@ -18,7 +18,7 @@ namespace Migrations
{ {
public sealed class MigrationPath : IMigrationPath public sealed class MigrationPath : IMigrationPath
{ {
private const int CurrentVersion = 23; private const int CurrentVersion = 24;
private readonly IServiceProvider serviceProvider; private readonly IServiceProvider serviceProvider;
public MigrationPath(IServiceProvider serviceProvider) public MigrationPath(IServiceProvider serviceProvider)
@ -74,7 +74,8 @@ namespace Migrations
} }
// Version 12: Introduce roles. // Version 12: Introduce roles.
if (version < 12) // Version 24: Improve a naming in the languages config.
if (version < 24)
{ {
yield return serviceProvider.GetRequiredService<RebuildApps>(); yield return serviceProvider.GetRequiredService<RebuildApps>();
} }

2
backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs

@ -42,7 +42,7 @@ namespace Squidex.Domain.Apps.Core.Apps
if (ContainsKey(id)) if (ContainsKey(id))
{ {
throw new ArgumentException("Id already exists.", nameof(id)); return this;
} }
var newClient = new AppClient(id, secret) var newClient = new AppClient(id, secret)

4
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/DependencyInjectionExtensions.cs

@ -14,9 +14,7 @@ namespace Microsoft.Extensions.DependencyInjection
{ {
public static IServiceCollection AddRuleAction<TAction, THandler>(this IServiceCollection services) where THandler : class, IRuleActionHandler where TAction : RuleAction public static IServiceCollection AddRuleAction<TAction, THandler>(this IServiceCollection services) where THandler : class, IRuleActionHandler where TAction : RuleAction
{ {
services.AddSingletonAs<THandler>() services.AddSingleton<IRuleActionHandler, THandler>();
.As<IRuleActionHandler>();
services.AddSingleton(new RuleActionRegistration(typeof(TAction))); services.AddSingleton(new RuleActionRegistration(typeof(TAction)));
return services; return services;

4
backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj

@ -20,8 +20,8 @@
<PackageReference Include="HtmlAgilityPack" Version="1.11.29" /> <PackageReference Include="HtmlAgilityPack" Version="1.11.29" />
<PackageReference Include="Markdig" Version="0.22.1" /> <PackageReference Include="Markdig" Version="0.22.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
<PackageReference Include="Microsoft.OData.Core" Version="7.7.3" /> <PackageReference Include="Microsoft.OData.Core" Version="7.8.1" />
<PackageReference Include="NJsonSchema" Version="10.3.1" /> <PackageReference Include="NJsonSchema" Version="10.3.2" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.Jint" Version="3.0.0-beta-0" /> <PackageReference Include="Squidex.Jint" Version="3.0.0-beta-0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />

1
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs

@ -18,6 +18,7 @@ using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Domain.Apps.Entities.Contents.Text; using Squidex.Domain.Apps.Entities.Contents.Text;
using Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations; using Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Hosting;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Queries; using Squidex.Infrastructure.Queries;

20
backend/src/Squidex.Domain.Apps.Entities/Assets/AssetStats.cs

@ -7,22 +7,14 @@
using System; using System;
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
namespace Squidex.Domain.Apps.Entities.Assets namespace Squidex.Domain.Apps.Entities.Assets
{ {
public sealed class AssetStats public sealed record AssetStats(
DateTime Date,
long TotalCount,
long TotalSize)
{ {
public DateTime Date { get; }
public long TotalCount { get; }
public long TotalSize { get; }
public AssetStats(DateTime date, long totalCount, long totalSize)
{
Date = date;
TotalCount = totalCount;
TotalSize = totalSize;
}
} }
} }

1
backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Elastic/ElasticSearchTextIndex.cs

@ -13,6 +13,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Elasticsearch.Net; using Elasticsearch.Net;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Hosting;
using Squidex.Infrastructure; using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents.Text.Elastic namespace Squidex.Domain.Apps.Entities.Contents.Text.Elastic

18
backend/src/Squidex.Domain.Apps.Entities/History/NotifoService.cs

@ -7,7 +7,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using NodaTime; using NodaTime;
@ -26,14 +25,14 @@ using Squidex.Shared.Users;
namespace Squidex.Domain.Apps.Entities.History namespace Squidex.Domain.Apps.Entities.History
{ {
public class NotifoService : IInitializable, IUserEventHandler public class NotifoService : IUserEventHandler
{ {
private static readonly Duration MaxAge = Duration.FromHours(12); private static readonly Duration MaxAge = Duration.FromHours(12);
private readonly NotifoOptions options; private readonly NotifoOptions options;
private readonly IUrlGenerator urlGenerator; private readonly IUrlGenerator urlGenerator;
private readonly IUserResolver userResolver; private readonly IUserResolver userResolver;
private readonly IClock clock; private readonly IClock clock;
private INotifoClient? client; private readonly INotifoClient? client;
public NotifoService(IOptions<NotifoOptions> options, IUrlGenerator urlGenerator, IUserResolver userResolver, IClock clock) public NotifoService(IOptions<NotifoOptions> options, IUrlGenerator urlGenerator, IUserResolver userResolver, IClock clock)
{ {
@ -48,25 +47,20 @@ namespace Squidex.Domain.Apps.Entities.History
this.userResolver = userResolver; this.userResolver = userResolver;
this.clock = clock; this.clock = clock;
}
public Task InitializeAsync(CancellationToken ct = default) if (options.Value.IsConfigured())
{
if (options.IsConfigured())
{ {
var builder = var builder =
NotifoClientBuilder.Create() NotifoClientBuilder.Create()
.SetApiKey(options.ApiKey); .SetApiKey(options.Value.ApiKey);
if (!string.IsNullOrWhiteSpace(options.ApiUrl)) if (!string.IsNullOrWhiteSpace(options.Value.ApiUrl))
{ {
builder = builder.SetApiUrl(options.ApiUrl); builder = builder.SetApiUrl(options.Value.ApiUrl);
} }
client = builder.Build(); client = builder.Build();
} }
return Task.CompletedTask;
} }
public void OnUserUpdated(IUser user) public void OnUserUpdated(IUser user)

6
backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/RuleRunnerGrain.cs

@ -17,9 +17,11 @@ using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.States; using Squidex.Infrastructure.States;
using Squidex.Infrastructure.Tasks;
using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.Translations;
using Squidex.Log; using Squidex.Log;
using TaskExtensions = Squidex.Infrastructure.Tasks.TaskExtensions;
#pragma warning disable RECS0015 // If an extension method is called as static method convert it to method syntax
namespace Squidex.Domain.Apps.Entities.Rules.Runner namespace Squidex.Domain.Apps.Entities.Rules.Runner
{ {
@ -151,7 +153,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Runner
private void Process(State job, CancellationToken ct) private void Process(State job, CancellationToken ct)
{ {
ProcessAsync(job, ct).Forget(); TaskExtensions.Forget(ProcessAsync(job, ct));
} }
private async Task ProcessAsync(State currentState, CancellationToken ct) private async Task ProcessAsync(State currentState, CancellationToken ct)

2
backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj

@ -17,7 +17,7 @@
<ProjectReference Include="..\Squidex.Shared\Squidex.Shared.csproj" /> <ProjectReference Include="..\Squidex.Shared\Squidex.Shared.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="CsvHelper" Version="18.0.0" /> <PackageReference Include="CsvHelper" Version="19.0.0" />
<PackageReference Include="Elasticsearch.Net" Version="7.10.1" /> <PackageReference Include="Elasticsearch.Net" Version="7.10.1" />
<PackageReference Include="Equals.Fody" Version="4.0.1" PrivateAssets="all" /> <PackageReference Include="Equals.Fody" Version="4.0.1" PrivateAssets="all" />
<PackageReference Include="Fody" Version="6.3.0"> <PackageReference Include="Fody" Version="6.3.0">

1
backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore.cs

@ -12,6 +12,7 @@ using System.Threading.Tasks;
using Microsoft.Azure.Documents; using Microsoft.Azure.Documents;
using Microsoft.Azure.Documents.Client; using Microsoft.Azure.Documents.Client;
using Newtonsoft.Json; using Newtonsoft.Json;
using Squidex.Hosting;
using Index = Microsoft.Azure.Documents.Index; using Index = Microsoft.Azure.Documents.Index;
namespace Squidex.Infrastructure.EventSourcing namespace Squidex.Infrastructure.EventSourcing

1
backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Reader.cs

@ -10,6 +10,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Hosting;
using Squidex.Log; using Squidex.Log;
namespace Squidex.Infrastructure.EventSourcing namespace Squidex.Infrastructure.EventSourcing

2
backend/src/Squidex.Infrastructure.Azure/Squidex.Infrastructure.Azure.csproj

@ -7,7 +7,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Azure.DocumentDB.ChangeFeedProcessor" Version="2.3.2" /> <PackageReference Include="Microsoft.Azure.DocumentDB.ChangeFeedProcessor" Version="2.3.2" />
<PackageReference Include="Microsoft.Azure.DocumentDB.Core" Version="2.13.0" /> <PackageReference Include="Microsoft.Azure.DocumentDB.Core" Version="2.13.1" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="Microsoft.Azure.Storage.Blob" Version="11.2.2" /> <PackageReference Include="Microsoft.Azure.Storage.Blob" Version="11.2.2" />

6
backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs

@ -12,6 +12,8 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using EventStore.ClientAPI; using EventStore.ClientAPI;
using EventStore.ClientAPI.Exceptions; using EventStore.ClientAPI.Exceptions;
using Squidex.Hosting;
using Squidex.Hosting.Configuration;
using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json;
using Squidex.Log; using Squidex.Log;
@ -48,7 +50,9 @@ namespace Squidex.Infrastructure.EventSourcing
} }
catch (Exception ex) catch (Exception ex)
{ {
throw new ConfigurationException("Cannot connect to event store.", ex); var error = new ConfigurationError("GetEventStore cannot connect to event store.");
throw new ConfigurationException(error, ex);
} }
await projectionClient.ConnectAsync(); await projectionClient.ConnectAsync();

5
backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionClient.cs

@ -14,6 +14,7 @@ using System.Threading.Tasks;
using EventStore.ClientAPI; using EventStore.ClientAPI;
using EventStore.ClientAPI.Exceptions; using EventStore.ClientAPI.Exceptions;
using EventStore.ClientAPI.Projections; using EventStore.ClientAPI.Projections;
using Squidex.Hosting.Configuration;
using Squidex.Text; using Squidex.Text;
namespace Squidex.Infrastructure.EventSourcing namespace Squidex.Infrastructure.EventSourcing
@ -102,7 +103,9 @@ namespace Squidex.Infrastructure.EventSourcing
} }
catch (Exception ex) catch (Exception ex)
{ {
throw new ConfigurationException($"Cannot connect to event store projections: {projectionHost}.", ex); var error = new ConfigurationError($"GetEventStore cannot connect to event store projections: {projectionHost}.");
throw new ConfigurationException(error, ex);
} }
} }

1
backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs

@ -11,7 +11,6 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow; using System.Threading.Tasks.Dataflow;
using MongoDB.Bson; using MongoDB.Bson;
using MongoDB.Bson.Serialization;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Infrastructure.States; using Squidex.Infrastructure.States;

6
backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs

@ -10,6 +10,8 @@ using System.Globalization;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Hosting;
using Squidex.Hosting.Configuration;
#pragma warning disable RECS0108 // Warns about static fields in generic types #pragma warning disable RECS0108 // Warns about static fields in generic types
@ -112,7 +114,9 @@ namespace Squidex.Infrastructure.MongoDb
} }
catch (Exception ex) catch (Exception ex)
{ {
throw new ConfigurationException($"MongoDb connection failed to connect to database {Database.DatabaseNamespace.DatabaseName}", ex); var error = new ConfigurationError($"MongoDb connection failed to connect to database {Database.DatabaseNamespace.DatabaseName}.");
throw new ConfigurationException(error, ex);
} }
} }

12
backend/src/Squidex.Infrastructure.RabbitMq/CQRS/Events/RabbitMqEventConsumer.cs

@ -10,6 +10,8 @@ using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using RabbitMQ.Client; using RabbitMQ.Client;
using Squidex.Hosting;
using Squidex.Hosting.Configuration;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json;
@ -69,14 +71,18 @@ namespace Squidex.Infrastructure.CQRS.Events
if (!currentConnection.IsOpen) if (!currentConnection.IsOpen)
{ {
throw new ConfigurationException($"RabbitMq event bus failed to connect to {connectionFactory.Endpoint}"); var error = new ConfigurationError($"RabbitMq event bus failed to connect to {connectionFactory.Endpoint}.");
throw new ConfigurationException(error);
} }
return Task.CompletedTask; return Task.CompletedTask;
} }
catch (Exception e) catch (Exception ex)
{ {
throw new ConfigurationException($"RabbitMq event bus failed to connect to {connectionFactory.Endpoint}", e); var error = new ConfigurationError($"RabbitMq event bus failed to connect to {connectionFactory.Endpoint}.");
throw new ConfigurationException(error, ex);
} }
} }

20
backend/src/Squidex.Infrastructure/Configuration/Alternatives.cs

@ -1,20 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
namespace Microsoft.Extensions.Configuration
{
public sealed class Alternatives : Dictionary<string, Action>
{
public Alternatives()
: base(StringComparer.OrdinalIgnoreCase)
{
}
}
}

70
backend/src/Squidex.Infrastructure/Configuration/ConfigurationExtensions.cs

@ -1,70 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Globalization;
using System.Linq;
using Squidex.Infrastructure;
using Squidex.Text;
namespace Microsoft.Extensions.Configuration
{
public static class ConfigurationExtensions
{
public static T GetOptionalValue<T>(this IConfiguration config, string path, T defaultValue = default)
{
var value = config.GetValue(path, defaultValue!);
return value;
}
public static int GetOptionalValue(this IConfiguration config, string path, int defaultValue)
{
var value = config.GetValue<string>(path);
if (string.IsNullOrWhiteSpace(value) || !int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
{
result = defaultValue;
}
return result;
}
public static string GetRequiredValue(this IConfiguration config, string path)
{
var value = config.GetValue<string>(path);
if (string.IsNullOrWhiteSpace(value))
{
var name = string.Join(" ", path.Split(':').Select(x => x.ToPascalCase()));
throw new ConfigurationException($"Configure the {name} with '{path}'.");
}
return value;
}
public static string ConfigureByOption(this IConfiguration config, string path, Alternatives options)
{
var value = config.GetRequiredValue(path);
if (options.TryGetValue(value, out var action))
{
action();
}
else if (options.TryGetValue("default", out action))
{
action();
}
else
{
throw new ConfigurationException($"Unsupported value '{value}' for '{path}', supported: {string.Join(" ", options.Keys)}.");
}
return value;
}
}
}

26
backend/src/Squidex.Infrastructure/ConfigurationException.cs

@ -1,26 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Runtime.Serialization;
namespace Squidex.Infrastructure
{
[Serializable]
public class ConfigurationException : Exception
{
public ConfigurationException(string message, Exception? inner = null)
: base(message, inner)
{
}
protected ConfigurationException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
}

38
backend/src/Squidex.Infrastructure/DelegateInitializer.cs

@ -1,38 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Squidex.Infrastructure
{
public sealed class DelegateInitializer : IInitializable
{
private readonly string name;
private readonly Func<CancellationToken, Task> action;
public DelegateInitializer(string name, Func<CancellationToken, Task> action)
{
Guard.NotNull(action, nameof(action));
this.name = name;
this.action = action;
}
public override string ToString()
{
return name;
}
public Task InitializeAsync(CancellationToken ct = default)
{
return action(ct);
}
}
}

110
backend/src/Squidex.Infrastructure/DependencyInjection/DependencyInjectionExtensions.cs

@ -1,110 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Linq;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Squidex.Infrastructure;
namespace Microsoft.Extensions.DependencyInjection
{
public static class DependencyInjectionExtensions
{
public delegate void Registrator(Type serviceType, Func<IServiceProvider, object> implementationFactory);
public sealed class InterfaceRegistrator<T> where T : notnull
{
private readonly Registrator register;
private readonly Registrator registerOptional;
public InterfaceRegistrator(Registrator register, Registrator registerOptional)
{
this.register = register;
this.registerOptional = registerOptional;
var interfaces = typeof(T).GetInterfaces();
if (interfaces.Contains(typeof(IInitializable)))
{
register(typeof(IInitializable), c => c.GetRequiredService<T>());
}
if (interfaces.Contains(typeof(IBackgroundProcess)))
{
register(typeof(IBackgroundProcess), c => c.GetRequiredService<T>());
}
}
public InterfaceRegistrator<T> AsSelf()
{
return this;
}
public InterfaceRegistrator<T> AsOptional<TInterface>()
{
if (typeof(TInterface) != typeof(T))
{
registerOptional(typeof(TInterface), c => c.GetRequiredService<T>());
}
return this;
}
public InterfaceRegistrator<T> As<TInterface>()
{
if (typeof(TInterface) != typeof(T))
{
register(typeof(TInterface), c => c.GetRequiredService<T>());
}
return this;
}
}
public static InterfaceRegistrator<T> AddTransientAs<T>(this IServiceCollection services, Func<IServiceProvider, T> factory) where T : class
{
services.AddTransient(typeof(T), factory);
return new InterfaceRegistrator<T>((t, f) => services.AddTransient(t, f), services.TryAddTransient);
}
public static InterfaceRegistrator<T> AddTransientAs<T>(this IServiceCollection services) where T : class
{
services.AddTransient<T, T>();
return new InterfaceRegistrator<T>((t, f) => services.AddTransient(t, f), services.TryAddTransient);
}
public static InterfaceRegistrator<T> AddSingletonAs<T>(this IServiceCollection services, Func<IServiceProvider, T> factory) where T : class
{
services.AddSingleton(typeof(T), factory);
return new InterfaceRegistrator<T>((t, f) => services.AddSingleton(t, f), services.TryAddSingleton);
}
public static InterfaceRegistrator<T> AddSingletonAs<T>(this IServiceCollection services) where T : class
{
services.AddSingleton<T, T>();
return new InterfaceRegistrator<T>((t, f) => services.AddSingleton(t, f), services.TryAddSingleton);
}
public static InterfaceRegistrator<T> AddScopedAs<T>(this IServiceCollection services, Func<IServiceProvider, T> factory) where T : class
{
services.AddScoped(typeof(T), factory);
return new InterfaceRegistrator<T>((t, f) => services.AddScoped(t, f), services.TryAddScoped);
}
public static InterfaceRegistrator<T> AddScopedAs<T>(this IServiceCollection services) where T : class
{
services.AddScoped<T, T>();
return new InterfaceRegistrator<T>((t, f) => services.AddScoped(t, f), services.TryAddScoped);
}
}
}

17
backend/src/Squidex.Infrastructure/IBackgroundProcess.cs

@ -1,17 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading;
using System.Threading.Tasks;
namespace Squidex.Infrastructure
{
public interface IBackgroundProcess
{
Task StartAsync(CancellationToken ct);
}
}

19
backend/src/Squidex.Infrastructure/IInitializable.cs

@ -1,19 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading;
using System.Threading.Tasks;
namespace Squidex.Infrastructure
{
public interface IInitializable
{
int Order => 0;
Task InitializeAsync(CancellationToken ct = default);
}
}

1
backend/src/Squidex.Infrastructure/LanguagesInitializer.cs

@ -8,6 +8,7 @@
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Squidex.Hosting;
namespace Squidex.Infrastructure namespace Squidex.Infrastructure
{ {

8
backend/src/Squidex.Infrastructure/Orleans/GrainBootstrap.cs

@ -9,6 +9,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Orleans; using Orleans;
using Orleans.Runtime; using Orleans.Runtime;
using Squidex.Hosting;
namespace Squidex.Infrastructure.Orleans namespace Squidex.Infrastructure.Orleans
{ {
@ -17,6 +18,8 @@ namespace Squidex.Infrastructure.Orleans
private const int NumTries = 10; private const int NumTries = 10;
private readonly IGrainFactory grainFactory; private readonly IGrainFactory grainFactory;
public string Name => typeof(T).Name;
public GrainBootstrap(IGrainFactory grainFactory) public GrainBootstrap(IGrainFactory grainFactory)
{ {
Guard.NotNull(grainFactory, nameof(grainFactory)); Guard.NotNull(grainFactory, nameof(grainFactory));
@ -46,10 +49,5 @@ namespace Squidex.Infrastructure.Orleans
} }
} }
} }
public override string ToString()
{
return typeof(T).ToString();
}
} }
} }

7
backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj

@ -18,7 +18,7 @@
<PackageReference Include="McMaster.NETCore.Plugins" Version="1.3.1" /> <PackageReference Include="McMaster.NETCore.Plugins" Version="1.3.1" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions" Version="5.0.1" /> <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions" Version="5.0.1" />
<PackageReference Include="Microsoft.OData.Core" Version="7.7.3" /> <PackageReference Include="Microsoft.OData.Core" Version="7.8.1" />
<PackageReference Include="Microsoft.Orleans.CodeGenerator.MSBuild" Version="3.3.0"> <PackageReference Include="Microsoft.Orleans.CodeGenerator.MSBuild" Version="3.3.0">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
@ -26,10 +26,11 @@
<PackageReference Include="Microsoft.Orleans.Core" Version="3.3.0" /> <PackageReference Include="Microsoft.Orleans.Core" Version="3.3.0" />
<PackageReference Include="Microsoft.Orleans.OrleansRuntime" Version="3.3.0" /> <PackageReference Include="Microsoft.Orleans.OrleansRuntime" Version="3.3.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="NJsonSchema" Version="10.3.1" /> <PackageReference Include="NJsonSchema" Version="10.3.2" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.Assets" Version="1.3.0" /> <PackageReference Include="Squidex.Assets" Version="1.3.0" />
<PackageReference Include="Squidex.Caching" Version="1.1.0" /> <PackageReference Include="Squidex.Caching" Version="1.3.0" />
<PackageReference Include="Squidex.Hosting.Abstractions" Version="1.7.0" />
<PackageReference Include="Squidex.Log" Version="1.1.0" /> <PackageReference Include="Squidex.Log" Version="1.1.0" />
<PackageReference Include="Squidex.Text" Version="1.5.0" /> <PackageReference Include="Squidex.Text" Version="1.5.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />

24
backend/src/Squidex.Infrastructure/UsageTracking/ApiStats.cs

@ -7,26 +7,14 @@
using System; using System;
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
namespace Squidex.Infrastructure.UsageTracking namespace Squidex.Infrastructure.UsageTracking
{ {
public sealed class ApiStats public sealed record ApiStats(
DateTime Date,
long TotalCalls, double AverageElapsedMs,
long TotalBytes)
{ {
public DateTime Date { get; }
public long TotalCalls { get; }
public long TotalBytes { get; }
public double AverageElapsedMs { get; }
public ApiStats(DateTime date, long totalCalls, double averageElapsedMs, long totalBytes)
{
Date = date;
TotalCalls = totalCalls;
TotalBytes = totalBytes;
AverageElapsedMs = averageElapsedMs;
}
} }
} }

29
backend/src/Squidex.Infrastructure/UsageTracking/ApiStatsSummary.cs

@ -5,29 +5,16 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
namespace Squidex.Infrastructure.UsageTracking namespace Squidex.Infrastructure.UsageTracking
{ {
public sealed class ApiStatsSummary public sealed record ApiStatsSummary(
double AverageElapsedMs,
long TotalCalls,
long TotalBytes,
long MonthCalls,
long MonthBytes)
{ {
public long TotalCalls { get; }
public long TotalBytes { get; }
public long MonthCalls { get; }
public long MonthBytes { get; }
public double AverageElapsedMs { get; }
public ApiStatsSummary(double averageElapsedMs, long totalCalls, long totalBytes, long monthCalls, long monthBytes)
{
TotalCalls = totalCalls;
TotalBytes = totalBytes;
MonthCalls = monthCalls;
MonthBytes = monthBytes;
AverageElapsedMs = averageElapsedMs;
}
} }
} }

19
backend/src/Squidex.Infrastructure/UsageTracking/StoredUsage.cs

@ -7,24 +7,11 @@
using System; using System;
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
namespace Squidex.Infrastructure.UsageTracking namespace Squidex.Infrastructure.UsageTracking
{ {
public sealed class StoredUsage public sealed record StoredUsage(string? Category, DateTime Date, Counters Counters)
{ {
public string? Category { get; }
public DateTime Date { get; }
public Counters Counters { get; }
public StoredUsage(string? category, DateTime date, Counters counters)
{
Guard.NotNull(counters, nameof(counters));
Category = category;
Counters = counters;
Date = date;
}
} }
} }

44
backend/src/Squidex.Web/Pipeline/CleanupHostMiddleware.cs

@ -1,44 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
namespace Squidex.Web.Pipeline
{
public class CleanupHostMiddleware
{
private readonly RequestDelegate next;
public CleanupHostMiddleware(RequestDelegate next)
{
this.next = next;
}
public Task InvokeAsync(HttpContext context)
{
var request = context.Request;
if (request.Host.HasValue && (HasHttpsPort(request) || HasHttpPort(request)))
{
request.Host = new HostString(request.Host.Host);
}
return next(context);
}
private static bool HasHttpPort(HttpRequest request)
{
return request.Scheme == "http" && request.Host.Port == 80;
}
private static bool HasHttpsPort(HttpRequest request)
{
return request.Scheme == "https" && request.Host.Port == 443;
}
}
}

58
backend/src/Squidex.Web/Services/UrlGenerator.cs

@ -5,29 +5,29 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using Microsoft.Extensions.Options;
using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Assets; using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using IGenericUrlGenerator = Squidex.Hosting.IUrlGenerator;
namespace Squidex.Web.Services namespace Squidex.Web.Services
{ {
public sealed class UrlGenerator : IUrlGenerator public sealed class UrlGenerator : IUrlGenerator
{ {
private readonly IAssetFileStore assetFileStore; private readonly IAssetFileStore assetFileStore;
private readonly UrlsOptions urlsOptions; private readonly IGenericUrlGenerator urlGenerator;
public bool CanGenerateAssetSourceUrl { get; } public bool CanGenerateAssetSourceUrl { get; }
public UrlGenerator(IOptions<UrlsOptions> urlsOptions, IAssetFileStore assetFileStore, bool allowAssetSourceUrl) public UrlGenerator(IGenericUrlGenerator urlGenerator, IAssetFileStore assetFileStore, bool allowAssetSourceUrl)
{ {
Guard.NotNull(assetFileStore, nameof(assetFileStore)); Guard.NotNull(assetFileStore, nameof(assetFileStore));
Guard.NotNull(urlsOptions, nameof(urlsOptions)); Guard.NotNull(urlGenerator, nameof(urlGenerator));
this.assetFileStore = assetFileStore; this.assetFileStore = assetFileStore;
this.urlsOptions = urlsOptions.Value; this.urlGenerator = urlGenerator;
CanGenerateAssetSourceUrl = allowAssetSourceUrl; CanGenerateAssetSourceUrl = allowAssetSourceUrl;
} }
@ -39,32 +39,32 @@ namespace Squidex.Web.Services
return null; return null;
} }
return urlsOptions.BuildUrl($"api/assets/{appId.Name}/{idOrSlug}?width=100&mode=Max"); return urlGenerator.BuildUrl($"api/assets/{appId.Name}/{idOrSlug}?width=100&mode=Max");
} }
public string AppSettingsUI(NamedId<DomainId> appId) public string AppSettingsUI(NamedId<DomainId> appId)
{ {
return urlsOptions.BuildUrl($"app/{appId.Name}/settings", false); return urlGenerator.BuildUrl($"app/{appId.Name}/settings", false);
} }
public string AssetContentBase() public string AssetContentBase()
{ {
return urlsOptions.BuildUrl("api/assets/"); return urlGenerator.BuildUrl("api/assets/");
} }
public string AssetContentBase(string appName) public string AssetContentBase(string appName)
{ {
return urlsOptions.BuildUrl($"api/assets/{appName}/"); return urlGenerator.BuildUrl($"api/assets/{appName}/");
} }
public string AssetContent(NamedId<DomainId> appId, DomainId assetId) public string AssetContent(NamedId<DomainId> appId, DomainId assetId)
{ {
return urlsOptions.BuildUrl($"api/assets/{appId.Name}/{assetId}"); return urlGenerator.BuildUrl($"api/assets/{appId.Name}/{assetId}");
} }
public string AssetContent(NamedId<DomainId> appId, string idOrSlug) public string AssetContent(NamedId<DomainId> appId, string idOrSlug)
{ {
return urlsOptions.BuildUrl($"api/assets/{appId.Name}/{idOrSlug}"); return urlGenerator.BuildUrl($"api/assets/{appId.Name}/{idOrSlug}");
} }
public string? AssetSource(NamedId<DomainId> appId, DomainId assetId, long fileVersion) public string? AssetSource(NamedId<DomainId> appId, DomainId assetId, long fileVersion)
@ -74,92 +74,92 @@ namespace Squidex.Web.Services
public string AssetsUI(NamedId<DomainId> appId, string? query = null) public string AssetsUI(NamedId<DomainId> appId, string? query = null)
{ {
return urlsOptions.BuildUrl($"app/{appId.Name}/assets", false) + query != null ? $"?query={query}" : string.Empty; return urlGenerator.BuildUrl($"app/{appId.Name}/assets", false) + query != null ? $"?query={query}" : string.Empty;
} }
public string AssetsUI(NamedId<Named> appId, string? query = null) public string AssetsUI(NamedId<Named> appId, string? query = null)
{ {
return urlsOptions.BuildUrl($"app/{appId.Name}/assets?query={query}", false); return urlGenerator.BuildUrl($"app/{appId.Name}/assets?query={query}", false);
} }
public string BackupsUI(NamedId<DomainId> appId) public string BackupsUI(NamedId<DomainId> appId)
{ {
return urlsOptions.BuildUrl($"app/{appId.Name}/settings/backups", false); return urlGenerator.BuildUrl($"app/{appId.Name}/settings/backups", false);
} }
public string ClientsUI(NamedId<DomainId> appId) public string ClientsUI(NamedId<DomainId> appId)
{ {
return urlsOptions.BuildUrl($"app/{appId.Name}/settings/clients", false); return urlGenerator.BuildUrl($"app/{appId.Name}/settings/clients", false);
} }
public string ContentsUI(NamedId<DomainId> appId) public string ContentsUI(NamedId<DomainId> appId)
{ {
return urlsOptions.BuildUrl($"app/{appId.Name}/content", false); return urlGenerator.BuildUrl($"app/{appId.Name}/content", false);
} }
public string ContentsUI(NamedId<DomainId> appId, NamedId<DomainId> schemaId) public string ContentsUI(NamedId<DomainId> appId, NamedId<DomainId> schemaId)
{ {
return urlsOptions.BuildUrl($"app/{appId.Name}/content/{schemaId.Name}", false); return urlGenerator.BuildUrl($"app/{appId.Name}/content/{schemaId.Name}", false);
} }
public string ContentUI(NamedId<DomainId> appId, NamedId<DomainId> schemaId, DomainId contentId) public string ContentUI(NamedId<DomainId> appId, NamedId<DomainId> schemaId, DomainId contentId)
{ {
return urlsOptions.BuildUrl($"app/{appId.Name}/content/{schemaId.Name}/{contentId}/history", false); return urlGenerator.BuildUrl($"app/{appId.Name}/content/{schemaId.Name}/{contentId}/history", false);
} }
public string ContributorsUI(NamedId<DomainId> appId) public string ContributorsUI(NamedId<DomainId> appId)
{ {
return urlsOptions.BuildUrl($"app/{appId.Name}/settings/contributors", false); return urlGenerator.BuildUrl($"app/{appId.Name}/settings/contributors", false);
} }
public string DashboardUI(NamedId<DomainId> appId) public string DashboardUI(NamedId<DomainId> appId)
{ {
return urlsOptions.BuildUrl($"app/{appId.Name}", false); return urlGenerator.BuildUrl($"app/{appId.Name}", false);
} }
public string LanguagesUI(NamedId<DomainId> appId) public string LanguagesUI(NamedId<DomainId> appId)
{ {
return urlsOptions.BuildUrl($"app/{appId.Name}/settings/languages", false); return urlGenerator.BuildUrl($"app/{appId.Name}/settings/languages", false);
} }
public string PatternsUI(NamedId<DomainId> appId) public string PatternsUI(NamedId<DomainId> appId)
{ {
return urlsOptions.BuildUrl($"app/{appId.Name}/settings/patterns", false); return urlGenerator.BuildUrl($"app/{appId.Name}/settings/patterns", false);
} }
public string PlansUI(NamedId<DomainId> appId) public string PlansUI(NamedId<DomainId> appId)
{ {
return urlsOptions.BuildUrl($"app/{appId.Name}/settings/plans", false); return urlGenerator.BuildUrl($"app/{appId.Name}/settings/plans", false);
} }
public string RolesUI(NamedId<DomainId> appId) public string RolesUI(NamedId<DomainId> appId)
{ {
return urlsOptions.BuildUrl($"app/{appId.Name}/settings/roles", false); return urlGenerator.BuildUrl($"app/{appId.Name}/settings/roles", false);
} }
public string RulesUI(NamedId<DomainId> appId) public string RulesUI(NamedId<DomainId> appId)
{ {
return urlsOptions.BuildUrl($"app/{appId.Name}/rules", false); return urlGenerator.BuildUrl($"app/{appId.Name}/rules", false);
} }
public string SchemasUI(NamedId<DomainId> appId) public string SchemasUI(NamedId<DomainId> appId)
{ {
return urlsOptions.BuildUrl($"app/{appId.Name}/schemas", false); return urlGenerator.BuildUrl($"app/{appId.Name}/schemas", false);
} }
public string SchemaUI(NamedId<DomainId> appId, NamedId<DomainId> schemaId) public string SchemaUI(NamedId<DomainId> appId, NamedId<DomainId> schemaId)
{ {
return urlsOptions.BuildUrl($"app/{appId.Name}/schemas/{schemaId.Name}", false); return urlGenerator.BuildUrl($"app/{appId.Name}/schemas/{schemaId.Name}", false);
} }
public string WorkflowsUI(NamedId<DomainId> appId) public string WorkflowsUI(NamedId<DomainId> appId)
{ {
return urlsOptions.BuildUrl($"app/{appId.Name}/settings/workflows", false); return urlGenerator.BuildUrl($"app/{appId.Name}/settings/workflows", false);
} }
public string UI() public string UI()
{ {
return urlsOptions.BuildUrl("app", false); return urlGenerator.BuildUrl("app", false);
} }
} }
} }

159
backend/src/Squidex.Web/UrlsOptions.cs

@ -1,159 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
using Squidex.Infrastructure;
namespace Squidex.Web
{
public sealed class UrlsOptions
{
private readonly HashSet<HostString> allTrustedHosts = new HashSet<HostString>();
private string baseUrl;
private string[]? trustedHosts;
public string[]? KnownProxies { get; set; }
public bool EnableForwardHeaders { get; set; } = true;
public bool EnforceHTTPS { get; set; } = false;
public bool EnforceHost { get; set; } = false;
public int? HttpsPort { get; set; } = 443;
public string BaseUrl
{
get
{
return baseUrl;
}
set
{
if (TryBuildHost(value, out var host))
{
allTrustedHosts.Add(host);
}
baseUrl = value;
}
}
public string[]? TrustedHosts
{
get
{
return trustedHosts;
}
set
{
if (trustedHosts != null)
{
foreach (var canidate in trustedHosts)
{
if (TryBuildHost(canidate, out var host))
{
allTrustedHosts.Add(host);
}
}
}
trustedHosts = value;
}
}
public bool IsAllowedHost(string? url)
{
if (!Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out var uri))
{
return false;
}
return IsAllowedHost(uri);
}
public bool IsAllowedHost(Uri uri)
{
if (!uri.IsAbsoluteUri)
{
return true;
}
return allTrustedHosts.Contains(BuildHost(uri));
}
public string BuildUrl(string path, bool trailingSlash = true)
{
if (string.IsNullOrWhiteSpace(BaseUrl))
{
throw new ConfigurationException("Configure BaseUrl with 'urls:baseUrl'.");
}
return BaseUrl.BuildFullUrl(path, trailingSlash);
}
public HostString BuildHost()
{
if (string.IsNullOrWhiteSpace(BaseUrl))
{
throw new ConfigurationException("Configure BaseUrl with 'urls:baseUrl'.");
}
if (!TryBuildHost(BaseUrl, out var host))
{
throw new ConfigurationException("Configure BaseUrl with 'urls:baseUrl' host name.");
}
return host;
}
private static bool TryBuildHost(string urlOrHost, out HostString host)
{
host = default;
if (string.IsNullOrWhiteSpace(urlOrHost))
{
return false;
}
if (Uri.TryCreate(urlOrHost, UriKind.Absolute, out var uri1))
{
host = BuildHost(uri1);
return true;
}
if (Uri.TryCreate($"http://{urlOrHost}", UriKind.Absolute, out var uri2))
{
host = BuildHost(uri2);
return true;
}
return false;
}
private static HostString BuildHost(Uri uri)
{
return BuildHost(uri.Host, uri.Port);
}
private static HostString BuildHost(string host, int port)
{
if (port == 443 || port == 80)
{
return new HostString(host.ToLowerInvariant());
}
else
{
return new HostString(host.ToLowerInvariant(), port);
}
}
}
}

6
backend/src/Squidex/Areas/Api/Config/OpenApi/CommonProcessor.cs

@ -6,10 +6,10 @@
// ========================================================================== // ==========================================================================
using System.Collections.Generic; using System.Collections.Generic;
using Microsoft.Extensions.Options;
using NSwag; using NSwag;
using NSwag.Generation.Processors; using NSwag.Generation.Processors;
using NSwag.Generation.Processors.Contexts; using NSwag.Generation.Processors.Contexts;
using Squidex.Hosting;
using Squidex.Web; using Squidex.Web;
namespace Squidex.Areas.Api.Config.OpenApi namespace Squidex.Areas.Api.Config.OpenApi
@ -24,9 +24,9 @@ namespace Squidex.Areas.Api.Config.OpenApi
Url = "https://docs.squidex.io" Url = "https://docs.squidex.io"
}; };
public CommonProcessor(ExposedValues exposedValues, IOptions<UrlsOptions> urlOptions) public CommonProcessor(ExposedValues exposedValues, IUrlGenerator urlGenerator)
{ {
logoUrl = urlOptions.Value.BuildUrl("images/logo-white.png", false); logoUrl = urlGenerator.BuildUrl("images/logo-white.png", false);
if (!exposedValues.TryGetValue("version", out version!) || version == null) if (!exposedValues.TryGetValue("version", out version!) || version == null)
{ {

10
backend/src/Squidex/Areas/Api/Config/OpenApi/SecurityProcessor.cs

@ -7,9 +7,9 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Microsoft.Extensions.Options;
using NSwag; using NSwag;
using NSwag.Generation.Processors.Security; using NSwag.Generation.Processors.Security;
using Squidex.Hosting;
using Squidex.Pipeline.OpenApi; using Squidex.Pipeline.OpenApi;
using Squidex.Web; using Squidex.Web;
@ -17,19 +17,19 @@ namespace Squidex.Areas.Api.Config.OpenApi
{ {
public sealed class SecurityProcessor : SecurityDefinitionAppender public sealed class SecurityProcessor : SecurityDefinitionAppender
{ {
public SecurityProcessor(IOptions<UrlsOptions> urlOptions) public SecurityProcessor(IUrlGenerator urlGenerator)
: base(Constants.SecurityDefinition, Enumerable.Empty<string>(), CreateOAuthSchema(urlOptions.Value)) : base(Constants.SecurityDefinition, Enumerable.Empty<string>(), CreateOAuthSchema(urlGenerator))
{ {
} }
private static OpenApiSecurityScheme CreateOAuthSchema(UrlsOptions urlOptions) private static OpenApiSecurityScheme CreateOAuthSchema(IUrlGenerator urlGenerator)
{ {
var security = new OpenApiSecurityScheme var security = new OpenApiSecurityScheme
{ {
Type = OpenApiSecuritySchemeType.OAuth2 Type = OpenApiSecuritySchemeType.OAuth2
}; };
var tokenUrl = urlOptions.BuildUrl($"{Constants.IdentityServerPrefix}/connect/token", false); var tokenUrl = urlGenerator.BuildUrl($"{Constants.IdentityServerPrefix}/connect/token", false);
security.TokenUrl = tokenUrl; security.TokenUrl = tokenUrl;

12
backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs

@ -10,11 +10,11 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Squidex.Areas.Api.Controllers.Statistics.Models; using Squidex.Areas.Api.Controllers.Statistics.Models;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.Plans; using Squidex.Domain.Apps.Entities.Apps.Plans;
using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Hosting;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.UsageTracking; using Squidex.Infrastructure.UsageTracking;
@ -34,16 +34,16 @@ namespace Squidex.Areas.Api.Controllers.Statistics
private readonly IAppPlansProvider appPlansProvider; private readonly IAppPlansProvider appPlansProvider;
private readonly IAssetUsageTracker assetStatsRepository; private readonly IAssetUsageTracker assetStatsRepository;
private readonly IDataProtector dataProtector; private readonly IDataProtector dataProtector;
private readonly UrlsOptions urlsOptions; private readonly IUrlGenerator urlGenerator;
public UsagesController( public UsagesController(
ICommandBus commandBus, ICommandBus commandBus,
IDataProtectionProvider dataProtection,
IApiUsageTracker usageTracker, IApiUsageTracker usageTracker,
IAppLogStore appLogStore, IAppLogStore appLogStore,
IAppPlansProvider appPlansProvider, IAppPlansProvider appPlansProvider,
IAssetUsageTracker assetStatsRepository, IAssetUsageTracker assetStatsRepository,
IDataProtectionProvider dataProtection, IUrlGenerator urlGenerator)
IOptions<UrlsOptions> urlsOptions)
: base(commandBus) : base(commandBus)
{ {
this.usageTracker = usageTracker; this.usageTracker = usageTracker;
@ -51,7 +51,7 @@ namespace Squidex.Areas.Api.Controllers.Statistics
this.appLogStore = appLogStore; this.appLogStore = appLogStore;
this.appPlansProvider = appPlansProvider; this.appPlansProvider = appPlansProvider;
this.assetStatsRepository = assetStatsRepository; this.assetStatsRepository = assetStatsRepository;
this.urlsOptions = urlsOptions.Value; this.urlGenerator = urlGenerator;
dataProtector = dataProtection.CreateProtector("LogToken"); dataProtector = dataProtection.CreateProtector("LogToken");
} }
@ -73,7 +73,7 @@ namespace Squidex.Areas.Api.Controllers.Statistics
{ {
var token = dataProtector.Protect(App.Id.ToString()); var token = dataProtector.Protect(App.Id.ToString());
var url = urlsOptions.BuildUrl($"/api/apps/log/{token}/"); var url = urlGenerator.BuildUrl($"/api/apps/log/{token}/");
var response = new LogDownloadDto { DownloadUrl = url }; var response = new LogDownloadDto { DownloadUrl = url };

1
backend/src/Squidex/Areas/Api/Controllers/Translations/Models/TranslationDto.cs

@ -6,7 +6,6 @@
// ========================================================================== // ==========================================================================
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.Translations;
using Squidex.Text.Translations; using Squidex.Text.Translations;
namespace Squidex.Areas.Api.Controllers.Translations.Models namespace Squidex.Areas.Api.Controllers.Translations.Models

1
backend/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs

@ -9,7 +9,6 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Squidex.Areas.Api.Controllers.Translations.Models; using Squidex.Areas.Api.Controllers.Translations.Models;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Translations;
using Squidex.Shared; using Squidex.Shared;
using Squidex.Text.Translations; using Squidex.Text.Translations;
using Squidex.Web; using Squidex.Web;

13
backend/src/Squidex/Areas/IdentityServer/Config/CreateAdminHost.cs → backend/src/Squidex/Areas/IdentityServer/Config/CreateAdminInitializer.cs

@ -14,8 +14,8 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Logging; using Microsoft.IdentityModel.Logging;
using Squidex.Config; using Squidex.Config;
using Squidex.Config.Startup;
using Squidex.Domain.Users; using Squidex.Domain.Users;
using Squidex.Hosting;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Security;
using Squidex.Log; using Squidex.Log;
@ -24,20 +24,21 @@ using Squidex.Shared.Users;
namespace Squidex.Areas.IdentityServer.Config namespace Squidex.Areas.IdentityServer.Config
{ {
public sealed class CreateAdminHost : SafeHostedService public sealed class CreateAdminInitializer : IInitializable
{ {
private readonly IServiceProvider serviceProvider; private readonly IServiceProvider serviceProvider;
private readonly MyIdentityOptions identityOptions; private readonly MyIdentityOptions identityOptions;
public CreateAdminHost(ISemanticLog log, IServiceProvider serviceProvider, IOptions<MyIdentityOptions> identityOptions) public int Order => int.MaxValue;
: base(log)
public CreateAdminInitializer(IServiceProvider serviceProvider, IOptions<MyIdentityOptions> identityOptions)
{ {
this.serviceProvider = serviceProvider; this.serviceProvider = serviceProvider;
this.identityOptions = identityOptions.Value; this.identityOptions = identityOptions.Value;
} }
protected override async Task StartAsync(ISemanticLog log, CancellationToken ct) public async Task InitializeAsync(CancellationToken ct)
{ {
IdentityModelEventSource.ShowPII = identityOptions.ShowPII; IdentityModelEventSource.ShowPII = identityOptions.ShowPII;
@ -91,6 +92,8 @@ namespace Squidex.Areas.IdentityServer.Config
} }
catch (Exception ex) catch (Exception ex)
{ {
var log = serviceProvider.GetRequiredService<ISemanticLog>();
log.LogError(ex, w => w log.LogError(ex, w => w
.WriteProperty("action", "createAdmin") .WriteProperty("action", "createAdmin")
.WriteProperty("status", "failed")); .WriteProperty("status", "failed"));

14
backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs

@ -15,7 +15,6 @@ using Microsoft.AspNetCore.DataProtection.KeyManagement;
using Microsoft.AspNetCore.DataProtection.Repositories; using Microsoft.AspNetCore.DataProtection.Repositories;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Squidex.Domain.Users; using Squidex.Domain.Users;
using Squidex.Shared.Identity; using Squidex.Shared.Identity;
using Squidex.Web; using Squidex.Web;
@ -27,15 +26,13 @@ namespace Squidex.Areas.IdentityServer.Config
{ {
public static void AddSquidexIdentityServer(this IServiceCollection services) public static void AddSquidexIdentityServer(this IServiceCollection services)
{ {
services.AddSingletonAs<IConfigureOptions<KeyManagementOptions>>(s => services.Configure<KeyManagementOptions>((c, options) =>
{ {
return new ConfigureOptions<KeyManagementOptions>(options => options.XmlRepository = c.GetRequiredService<IXmlRepository>();
{
options.XmlRepository = s.GetRequiredService<IXmlRepository>();
});
}); });
services.AddDataProtection().SetApplicationName("Squidex"); services.AddDataProtection()
.SetApplicationName("Squidex");
services.AddIdentity<IdentityUser, IdentityRole>() services.AddIdentity<IdentityUser, IdentityRole>()
.AddDefaultTokenProviders(); .AddDefaultTokenProviders();
@ -61,6 +58,9 @@ namespace Squidex.Areas.IdentityServer.Config
services.AddSingletonAs<InMemoryResourcesStore>() services.AddSingletonAs<InMemoryResourcesStore>()
.As<IResourceStore>(); .As<IResourceStore>();
services.AddSingletonAs<CreateAdminInitializer>()
.AsSelf();
services.AddIdentityServer(options => services.AddIdentityServer(options =>
{ {
options.UserInteraction.ErrorUrl = "/error/"; options.UserInteraction.ErrorUrl = "/error/";

39
backend/src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs

@ -18,6 +18,7 @@ using Squidex.Config;
using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Users; using Squidex.Domain.Users;
using Squidex.Hosting;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Security;
using Squidex.Shared; using Squidex.Shared;
@ -30,25 +31,15 @@ namespace Squidex.Areas.IdentityServer.Config
public class LazyClientStore : IClientStore public class LazyClientStore : IClientStore
{ {
private readonly IServiceProvider serviceProvider; private readonly IServiceProvider serviceProvider;
private readonly IAppProvider appProvider;
private readonly Dictionary<string, Client> staticClients = new Dictionary<string, Client>(StringComparer.OrdinalIgnoreCase); private readonly Dictionary<string, Client> staticClients = new Dictionary<string, Client>(StringComparer.OrdinalIgnoreCase);
public LazyClientStore( public LazyClientStore(IServiceProvider serviceProvider)
IServiceProvider serviceProvider,
IOptions<UrlsOptions> urlsOptions,
IOptions<MyIdentityOptions> identityOptions,
IAppProvider appProvider)
{ {
Guard.NotNull(appProvider, nameof(appProvider));
Guard.NotNull(identityOptions, nameof(identityOptions));
Guard.NotNull(serviceProvider, nameof(serviceProvider)); Guard.NotNull(serviceProvider, nameof(serviceProvider));
Guard.NotNull(urlsOptions, nameof(urlsOptions));
this.serviceProvider = serviceProvider; this.serviceProvider = serviceProvider;
this.appProvider = appProvider; CreateStaticClients();
CreateStaticClients(urlsOptions, identityOptions);
} }
public async Task<Client?> FindClientByIdAsync(string clientId) public async Task<Client?> FindClientByIdAsync(string clientId)
@ -62,6 +53,8 @@ namespace Squidex.Areas.IdentityServer.Config
var (appName, appClientId) = clientId.GetClientParts(); var (appName, appClientId) = clientId.GetClientParts();
var appProvider = serviceProvider.GetRequiredService<IAppProvider>();
if (!string.IsNullOrWhiteSpace(appName) && !string.IsNullOrWhiteSpace(appClientId)) if (!string.IsNullOrWhiteSpace(appName) && !string.IsNullOrWhiteSpace(appClientId))
{ {
var app = await appProvider.GetAppAsync(appName, true); var app = await appProvider.GetAppAsync(appName, true);
@ -136,15 +129,19 @@ namespace Squidex.Areas.IdentityServer.Config
}; };
} }
private void CreateStaticClients(IOptions<UrlsOptions> urlsOptions, IOptions<MyIdentityOptions> identityOptions) private void CreateStaticClients()
{ {
foreach (var client in CreateStaticClients(urlsOptions.Value, identityOptions.Value)) var identityOptions = serviceProvider.GetRequiredService<IOptions<MyIdentityOptions>>().Value;
var urlGenerator = serviceProvider.GetRequiredService<IUrlGenerator>();
foreach (var client in CreateStaticClients(urlGenerator, identityOptions))
{ {
staticClients[client.ClientId] = client; staticClients[client.ClientId] = client;
} }
} }
private static IEnumerable<Client> CreateStaticClients(UrlsOptions urlsOptions, MyIdentityOptions identityOptions) private static IEnumerable<Client> CreateStaticClients(IUrlGenerator urlGenerator, MyIdentityOptions identityOptions)
{ {
var frontendId = Constants.FrontendClient; var frontendId = Constants.FrontendClient;
@ -154,13 +151,13 @@ namespace Squidex.Areas.IdentityServer.Config
ClientName = frontendId, ClientName = frontendId,
RedirectUris = new List<string> RedirectUris = new List<string>
{ {
urlsOptions.BuildUrl("login;"), urlGenerator.BuildUrl("login;"),
urlsOptions.BuildUrl("client-callback-silent", false), urlGenerator.BuildUrl("client-callback-silent", false),
urlsOptions.BuildUrl("client-callback-popup", false) urlGenerator.BuildUrl("client-callback-popup", false)
}, },
PostLogoutRedirectUris = new List<string> PostLogoutRedirectUris = new List<string>
{ {
urlsOptions.BuildUrl("logout", false) urlGenerator.BuildUrl("logout", false)
}, },
AllowAccessTokensViaBrowser = true, AllowAccessTokensViaBrowser = true,
AllowedGrantTypes = GrantTypes.Implicit, AllowedGrantTypes = GrantTypes.Implicit,
@ -189,8 +186,8 @@ namespace Squidex.Areas.IdentityServer.Config
}, },
RedirectUris = new List<string> RedirectUris = new List<string>
{ {
urlsOptions.BuildUrl($"{Constants.PortalPrefix}/signin-internal", false), urlGenerator.BuildUrl($"{Constants.PortalPrefix}/signin-internal", false),
urlsOptions.BuildUrl($"{Constants.OrleansPrefix}/signin-internal", false) urlGenerator.BuildUrl($"{Constants.OrleansPrefix}/signin-internal", false)
}, },
AccessTokenLifetime = (int)TimeSpan.FromDays(30).TotalSeconds, AccessTokenLifetime = (int)TimeSpan.FromDays(30).TotalSeconds,
AllowedGrantTypes = GrantTypes.ImplicitAndClientCredentials, AllowedGrantTypes = GrantTypes.ImplicitAndClientCredentials,

9
backend/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs

@ -22,6 +22,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Squidex.Config; using Squidex.Config;
using Squidex.Domain.Users; using Squidex.Domain.Users;
using Squidex.Hosting;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Security;
using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.Translations;
@ -41,7 +42,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account
private readonly UserManager<IdentityUser> userManager; private readonly UserManager<IdentityUser> userManager;
private readonly IUserFactory userFactory; private readonly IUserFactory userFactory;
private readonly IUserEvents userEvents; private readonly IUserEvents userEvents;
private readonly UrlsOptions urlsOptions; private readonly IUrlGenerator urlGenerator;
private readonly MyIdentityOptions identityOptions; private readonly MyIdentityOptions identityOptions;
private readonly ISemanticLog log; private readonly ISemanticLog log;
private readonly IIdentityServerInteractionService interactions; private readonly IIdentityServerInteractionService interactions;
@ -51,7 +52,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account
UserManager<IdentityUser> userManager, UserManager<IdentityUser> userManager,
IUserFactory userFactory, IUserFactory userFactory,
IUserEvents userEvents, IUserEvents userEvents,
IOptions<UrlsOptions> urlsOptions, IUrlGenerator urlGenerator,
IOptions<MyIdentityOptions> identityOptions, IOptions<MyIdentityOptions> identityOptions,
ISemanticLog log, ISemanticLog log,
IIdentityServerInteractionService interactions) IIdentityServerInteractionService interactions)
@ -59,7 +60,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account
this.identityOptions = identityOptions.Value; this.identityOptions = identityOptions.Value;
this.interactions = interactions; this.interactions = interactions;
this.signInManager = signInManager; this.signInManager = signInManager;
this.urlsOptions = urlsOptions.Value; this.urlGenerator = urlGenerator;
this.userEvents = userEvents; this.userEvents = userEvents;
this.userFactory = userFactory; this.userFactory = userFactory;
this.userManager = userManager; this.userManager = userManager;
@ -406,7 +407,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account
private IActionResult RedirectToReturnUrl(string? returnUrl) private IActionResult RedirectToReturnUrl(string? returnUrl)
{ {
if (urlsOptions.IsAllowedHost(returnUrl) || interactions.IsValidReturnUrl(returnUrl)) if (urlGenerator.IsAllowedHost(returnUrl) || interactions.IsValidReturnUrl(returnUrl))
{ {
return Redirect(returnUrl); return Redirect(returnUrl);
} }

48
backend/src/Squidex/Config/Authentication/IdentityServerServices.cs

@ -5,13 +5,17 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using IdentityServer4; using IdentityServer4;
using IdentityServer4.AccessTokenValidation; using IdentityServer4.AccessTokenValidation;
using IdentityServer4.Hosting.LocalApiAuthentication;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Squidex.Hosting;
using Squidex.Web; using Squidex.Web;
namespace Squidex.Config.Authentication namespace Squidex.Config.Authentication
@ -20,25 +24,14 @@ namespace Squidex.Config.Authentication
{ {
public static AuthenticationBuilder AddSquidexIdentityServerAuthentication(this AuthenticationBuilder authBuilder, MyIdentityOptions identityOptions, IConfiguration config) public static AuthenticationBuilder AddSquidexIdentityServerAuthentication(this AuthenticationBuilder authBuilder, MyIdentityOptions identityOptions, IConfiguration config)
{ {
var apiAuthorityUrl = identityOptions.AuthorityUrl; var useCustomAuthorityUrl = !string.IsNullOrWhiteSpace(identityOptions.AuthorityUrl);
var useCustomAuthorityUrl = !string.IsNullOrWhiteSpace(apiAuthorityUrl);
if (!useCustomAuthorityUrl)
{
var urlsOptions = config.GetSection("urls").Get<UrlsOptions>();
apiAuthorityUrl = urlsOptions.BuildUrl(Constants.IdentityServerPrefix);
}
var apiScope = Constants.ApiScope;
if (useCustomAuthorityUrl) if (useCustomAuthorityUrl)
{ {
authBuilder.AddIdentityServerAuthentication(options => authBuilder.AddIdentityServerAuthentication(options =>
{ {
options.Authority = apiAuthorityUrl; options.Authority = identityOptions.AuthorityUrl;
options.ApiName = apiScope; options.ApiName = Constants.ApiScope;
options.ApiSecret = null; options.ApiSecret = null;
options.RequireHttpsMetadata = identityOptions.RequiresHttps; options.RequireHttpsMetadata = identityOptions.RequiresHttps;
options.SupportedTokens = SupportedTokens.Jwt; options.SupportedTokens = SupportedTokens.Jwt;
@ -46,19 +39,27 @@ namespace Squidex.Config.Authentication
} }
else else
{ {
var urlsOptions = config.GetSection("urls").Get<UrlsOptions>(); authBuilder.AddLocalApi();
authBuilder.AddLocalApi(options => authBuilder.Services.Configure<LocalApiAuthenticationOptions>((c, options) =>
{ {
options.ClaimsIssuer = urlsOptions.BuildUrl("/identity-server", false); options.ClaimsIssuer = GetAuthorityUrl(c);
options.ExpectedScope = apiScope; options.ExpectedScope = Constants.ApiScope;
}); });
} }
authBuilder.AddOpenIdConnect(options => authBuilder.Services.Configure<OpenIdConnectOptions>((c, options) =>
{ {
options.Authority = apiAuthorityUrl; if (!string.IsNullOrWhiteSpace(identityOptions.AuthorityUrl))
{
options.Authority = identityOptions.AuthorityUrl;
}
else
{
options.Authority = GetAuthorityUrl(c);
}
options.ClientId = Constants.InternalClientId; options.ClientId = Constants.InternalClientId;
options.ClientSecret = Constants.InternalClientSecret; options.ClientSecret = Constants.InternalClientSecret;
options.CallbackPath = "/signin-internal"; options.CallbackPath = "/signin-internal";
@ -85,5 +86,12 @@ namespace Squidex.Config.Authentication
return authBuilder; return authBuilder;
} }
private static string GetAuthorityUrl(IServiceProvider services)
{
var urlGenerator = services.GetRequiredService<IUrlGenerator>();
return urlGenerator.BuildUrl(Constants.IdentityServerPrefix, false);
}
} }
} }

3
backend/src/Squidex/Config/Authentication/IdentityServices.cs

@ -16,8 +16,7 @@ namespace Squidex.Config.Authentication
{ {
public static void AddSquidexIdentity(this IServiceCollection services, IConfiguration config) public static void AddSquidexIdentity(this IServiceCollection services, IConfiguration config)
{ {
services.Configure<MyIdentityOptions>( services.Configure<MyIdentityOptions>(config, "identity");
config.GetSection("identity"));
services.AddSingletonAs<DefaultUserResolver>() services.AddSingletonAs<DefaultUserResolver>()
.AsOptional<IUserResolver>(); .AsOptional<IUserResolver>();

6
backend/src/Squidex/Config/Domain/AssetServices.cs

@ -19,6 +19,7 @@ using Squidex.Domain.Apps.Entities.Assets.DomainObject;
using Squidex.Domain.Apps.Entities.Assets.Queries; using Squidex.Domain.Apps.Entities.Assets.Queries;
using Squidex.Domain.Apps.Entities.History; using Squidex.Domain.Apps.Entities.History;
using Squidex.Domain.Apps.Entities.Search; using Squidex.Domain.Apps.Entities.Search;
using Squidex.Hosting;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
@ -28,8 +29,7 @@ namespace Squidex.Config.Domain
{ {
public static void AddSquidexAssets(this IServiceCollection services, IConfiguration config) public static void AddSquidexAssets(this IServiceCollection services, IConfiguration config)
{ {
services.Configure<AssetOptions>( services.Configure<AssetOptions>(config, "assets");
config.GetSection("assets"));
if (config.GetValue<bool>("assets:deleteRecursive")) if (config.GetValue<bool>("assets:deleteRecursive"))
{ {
@ -162,7 +162,7 @@ namespace Squidex.Config.Domain
.As<IAssetThumbnailGenerator>(); .As<IAssetThumbnailGenerator>();
services.AddSingletonAs(c => new DelegateInitializer( services.AddSingletonAs(c => new DelegateInitializer(
c.GetRequiredService<IAssetStore>().GetType().FullName!, c.GetRequiredService<IAssetStore>().GetType().Name,
c.GetRequiredService<IAssetStore>().InitializeAsync)) c.GetRequiredService<IAssetStore>().InitializeAsync))
.As<IInitializable>(); .As<IInitializable>();
} }

3
backend/src/Squidex/Config/Domain/CommandsServices.cs

@ -32,8 +32,7 @@ namespace Squidex.Config.Domain
{ {
public static void AddSquidexCommands(this IServiceCollection services, IConfiguration config) public static void AddSquidexCommands(this IServiceCollection services, IConfiguration config)
{ {
services.Configure<ReadonlyOptions>( services.Configure<ReadonlyOptions>(config, "mode");
config.GetSection("mode"));
services.AddSingletonAs<InMemoryCommandBus>() services.AddSingletonAs<InMemoryCommandBus>()
.As<ICommandBus>(); .As<ICommandBus>();

3
backend/src/Squidex/Config/Domain/ContentsServices.cs

@ -27,8 +27,7 @@ namespace Squidex.Config.Domain
{ {
public static void AddSquidexContents(this IServiceCollection services, IConfiguration config) public static void AddSquidexContents(this IServiceCollection services, IConfiguration config)
{ {
services.Configure<ContentOptions>( services.Configure<ContentOptions>(config, "contents");
config.GetSection("contents"));
services.AddSingletonAs(c => new Lazy<IContentQueryService>(c.GetRequiredService<IContentQueryService>)) services.AddSingletonAs(c => new Lazy<IContentQueryService>(c.GetRequiredService<IContentQueryService>))
.AsSelf(); .AsSelf();

18
backend/src/Squidex/Config/Domain/EventPublishersServices.cs

@ -8,7 +8,7 @@
using System; using System;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Squidex.Infrastructure; using Squidex.Hosting.Configuration;
using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json;
@ -27,7 +27,9 @@ namespace Squidex.Config.Domain
if (string.IsNullOrWhiteSpace(eventPublisherType)) if (string.IsNullOrWhiteSpace(eventPublisherType))
{ {
throw new ConfigurationException($"Configure EventPublisher type with 'eventPublishers:{child.Key}:type'."); var error = new ConfigurationError("Value is required.", "eventPublishers:{child.Key}:type");
throw new ConfigurationException(error);
} }
var eventsFilter = child.GetValue<string>("eventsFilter"); var eventsFilter = child.GetValue<string>("eventsFilter");
@ -40,14 +42,18 @@ namespace Squidex.Config.Domain
if (string.IsNullOrWhiteSpace(publisherConfig)) if (string.IsNullOrWhiteSpace(publisherConfig))
{ {
throw new ConfigurationException($"Configure EventPublisher RabbitMq configuration with 'eventPublishers:{child.Key}:configuration'."); var error = new ConfigurationError("Value is required.", "eventPublishers:{child.Key}:configuration");
throw new ConfigurationException(error);
} }
var exchange = child.GetValue<string>("exchange"); var exchange = child.GetValue<string>("exchange");
if (string.IsNullOrWhiteSpace(exchange)) if (string.IsNullOrWhiteSpace(exchange))
{ {
throw new ConfigurationException($"Configure EventPublisher RabbitMq exchange with 'eventPublishers:{child.Key}:configuration'."); var error = new ConfigurationError("Value is required.", "eventPublishers:{child.Key}:exchange");
throw new ConfigurationException(error);
} }
var name = $"EventPublishers_{child.Key}"; var name = $"EventPublishers_{child.Key}";
@ -60,7 +66,9 @@ namespace Squidex.Config.Domain
} }
else else
{ {
throw new ConfigurationException($"Unsupported value '{child.Key}' for 'eventPublishers:{child.Key}:type', supported: RabbitMq."); var error = new ConfigurationError($"Unsupported value '{child.Key}", "eventPublishers:{child.Key}:type.");
throw new ConfigurationException(error);
} }
} }
} }

3
backend/src/Squidex/Config/Domain/HealthCheckServices.cs

@ -17,8 +17,7 @@ namespace Squidex.Config.Domain
{ {
public static void AddSquidexHealthChecks(this IServiceCollection services, IConfiguration config) public static void AddSquidexHealthChecks(this IServiceCollection services, IConfiguration config)
{ {
services.Configure<GCHealthCheckOptions>( services.Configure<GCHealthCheckOptions>(config, "healthz:gc");
config.GetSection("healthz:gc"));
services.AddHealthChecks() services.AddHealthChecks()
.AddCheck<GCHealthCheck>("GC", tags: new[] { "node" }) .AddCheck<GCHealthCheck>("GC", tags: new[] { "node" })

39
backend/src/Squidex/Config/Domain/InfrastructureServices.cs

@ -29,7 +29,6 @@ using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.EventSourcing.Grains; using Squidex.Infrastructure.EventSourcing.Grains;
using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.Translations;
using Squidex.Infrastructure.UsageTracking; using Squidex.Infrastructure.UsageTracking;
using Squidex.Pipeline.Robots; using Squidex.Pipeline.Robots;
using Squidex.Text.Translations; using Squidex.Text.Translations;
@ -43,12 +42,9 @@ namespace Squidex.Config.Domain
{ {
public static void AddSquidexInfrastructure(this IServiceCollection services, IConfiguration config) public static void AddSquidexInfrastructure(this IServiceCollection services, IConfiguration config)
{ {
services.Configure<UrlsOptions>( services.Configure<ExposedConfiguration>(config, "exposedConfiguration");
config.GetSection("urls"));
services.Configure<ExposedConfiguration>( services.Configure<ReplicatedCacheOptions>(config, "caching:replicated");
config.GetSection("exposedConfiguration"));
services.Configure<ReplicatedCacheOptions>(
config.GetSection("caching:replicated"));
services.AddReplicatedCache(); services.AddReplicatedCache();
services.AddAsyncLocalCache(); services.AddAsyncLocalCache();
@ -103,8 +99,7 @@ namespace Squidex.Config.Domain
public static void AddSquidexUsageTracking(this IServiceCollection services, IConfiguration config) public static void AddSquidexUsageTracking(this IServiceCollection services, IConfiguration config)
{ {
services.Configure<UsageOptions>( services.Configure<UsageOptions>(config, "usage");
config.GetSection("usage"));
services.AddSingletonAs(c => new CachingUsageTracker( services.AddSingletonAs(c => new CachingUsageTracker(
c.GetRequiredService<BackgroundUsageTracker>(), c.GetRequiredService<BackgroundUsageTracker>(),
@ -123,12 +118,11 @@ namespace Squidex.Config.Domain
public static void AddSquidexTranslation(this IServiceCollection services, IConfiguration config) public static void AddSquidexTranslation(this IServiceCollection services, IConfiguration config)
{ {
services.Configure<DeepLOptions>( services.Configure<GoogleCloudTranslationOptions>(config, "translations:googleCloud");
config.GetSection("translations:deepL"));
services.Configure<GoogleCloudTranslationOptions>( services.Configure<DeepLOptions>(config, "translations:deepL");
config.GetSection("translations:googleCloud"));
services.Configure<LanguagesOptions>( services.Configure<LanguagesOptions>(config, "languages");
config.GetSection("languages"));
services.AddSingletonAs<LanguagesInitializer>() services.AddSingletonAs<LanguagesInitializer>()
.AsSelf(); .AsSelf();
@ -145,14 +139,13 @@ namespace Squidex.Config.Domain
public static void AddSquidexControllerServices(this IServiceCollection services, IConfiguration config) public static void AddSquidexControllerServices(this IServiceCollection services, IConfiguration config)
{ {
services.Configure<RobotsTxtOptions>( services.Configure<RobotsTxtOptions>(config, "robots");
config.GetSection("robots"));
services.Configure<CachingOptions>( services.Configure<CachingOptions>(config, "caching");
config.GetSection("caching"));
services.Configure<MyUIOptions>( services.Configure<MyUIOptions>(config, "ui");
config.GetSection("ui"));
services.Configure<MyNewsOptions>( services.Configure<MyNewsOptions>(config, "news");
config.GetSection("news"));
services.AddSingletonAs<FeaturesService>() services.AddSingletonAs<FeaturesService>()
.AsSelf(); .AsSelf();

43
backend/src/Squidex/Config/Domain/LoggingServices.cs

@ -23,6 +23,7 @@ namespace Squidex.Config.Domain
public static void ConfigureForSquidex(this ILoggingBuilder builder, IConfiguration config) public static void ConfigureForSquidex(this ILoggingBuilder builder, IConfiguration config)
{ {
builder.ClearProviders(); builder.ClearProviders();
builder.ConfigureSemanticLog(config);
builder.AddConfiguration(config.GetSection("logging")); builder.AddConfiguration(config.GetSection("logging"));
@ -34,38 +35,9 @@ namespace Squidex.Config.Domain
private static void AddServices(this IServiceCollection services, IConfiguration config) private static void AddServices(this IServiceCollection services, IConfiguration config)
{ {
services.Configure<RequestLogOptions>( services.Configure<RequestLogOptions>(config, "logging");
config.GetSection("logging"));
services.Configure<RequestLogStoreOptions>( services.Configure<RequestLogStoreOptions>(config, "logging");
config.GetSection("logging"));
services.Configure<SemanticLogOptions>(
config.GetSection("logging"));
if (config.GetValue<bool>("logging:human"))
{
services.AddSingletonAs(_ => JsonLogWriterFactory.Readable())
.As<IObjectWriterFactory>();
}
else
{
services.AddSingletonAs(_ => JsonLogWriterFactory.Default())
.As<IObjectWriterFactory>();
}
var loggingFile = config.GetValue<string>("logging:file");
if (!string.IsNullOrWhiteSpace(loggingFile))
{
services.AddSingletonAs(_ => new FileChannel(loggingFile))
.As<ILogChannel>();
}
var useColors = config.GetValue<bool>("logging:colors");
services.AddSingletonAs(_ => new ConsoleLogChannel(useColors))
.As<ILogChannel>();
services.AddSingletonAs(_ => new ApplicationInfoLogAppender(typeof(LoggingServices).Assembly, Guid.NewGuid())) services.AddSingletonAs(_ => new ApplicationInfoLogAppender(typeof(LoggingServices).Assembly, Guid.NewGuid()))
.As<ILogAppender>(); .As<ILogAppender>();
@ -73,15 +45,6 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<ActionContextLogAppender>() services.AddSingletonAs<ActionContextLogAppender>()
.As<ILogAppender>(); .As<ILogAppender>();
services.AddSingletonAs<TimestampLogAppender>()
.As<ILogAppender>();
services.AddSingletonAs<DebugLogChannel>()
.As<ILogChannel>();
services.AddSingletonAs<SemanticLog>()
.As<ISemanticLog>();
services.AddSingletonAs<DefaultAppLogStore>() services.AddSingletonAs<DefaultAppLogStore>()
.As<IAppLogStore>(); .As<IAppLogStore>();

3
backend/src/Squidex/Config/Domain/MigrationServices.cs

@ -17,8 +17,7 @@ namespace Squidex.Config.Domain
{ {
public static void AddSquidexMigration(this IServiceCollection services, IConfiguration config) public static void AddSquidexMigration(this IServiceCollection services, IConfiguration config)
{ {
services.Configure<RebuildOptions>( services.Configure<RebuildOptions>(config, "rebuild");
config.GetSection("rebuild"));
services.AddSingletonAs<Migrator>() services.AddSingletonAs<Migrator>()
.AsSelf(); .AsSelf();

3
backend/src/Squidex/Config/Domain/NotificationsServices.cs

@ -25,8 +25,7 @@ namespace Squidex.Config.Domain
{ {
services.AddSingleton(Options.Create(emailOptions)); services.AddSingleton(Options.Create(emailOptions));
services.Configure<NotificationEmailTextOptions>( services.Configure<NotificationEmailTextOptions>(config, "email:notifications");
config.GetSection("email:notifications"));
services.AddSingletonAs<SmtpEmailSender>() services.AddSingletonAs<SmtpEmailSender>()
.As<IEmailSender>(); .As<IEmailSender>();

8
backend/src/Squidex/Config/Domain/QueryServices.cs

@ -8,11 +8,8 @@
using GraphQL.DataLoader; using GraphQL.DataLoader;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Contents.GraphQL; using Squidex.Domain.Apps.Entities.Contents.GraphQL;
using Squidex.Web;
using Squidex.Web.Services; using Squidex.Web.Services;
namespace Squidex.Config.Domain namespace Squidex.Config.Domain
@ -23,10 +20,7 @@ namespace Squidex.Config.Domain
{ {
var exposeSourceUrl = config.GetOptionalValue("assetStore:exposeSourceUrl", true); var exposeSourceUrl = config.GetOptionalValue("assetStore:exposeSourceUrl", true);
services.AddSingletonAs(c => new UrlGenerator( services.AddSingletonAs(c => ActivatorUtilities.CreateInstance<UrlGenerator>(c, exposeSourceUrl))
c.GetRequiredService<IOptions<UrlsOptions>>(),
c.GetRequiredService<IAssetFileStore>(),
exposeSourceUrl))
.As<IUrlGenerator>(); .As<IUrlGenerator>();
services.AddSingletonAs<DataLoaderContextAccessor>() services.AddSingletonAs<DataLoaderContextAccessor>()

3
backend/src/Squidex/Config/Domain/RuleServices.cs

@ -30,8 +30,7 @@ namespace Squidex.Config.Domain
{ {
public static void AddSquidexRules(this IServiceCollection services, IConfiguration config) public static void AddSquidexRules(this IServiceCollection services, IConfiguration config)
{ {
services.Configure<RuleOptions>( services.Configure<RuleOptions>(config, "rules");
config.GetSection("rules"));
services.AddTransientAs<RuleDomainObject>() services.AddTransientAs<RuleDomainObject>()
.AsSelf(); .AsSelf();

3
backend/src/Squidex/Config/Domain/SerializationInitializer.cs

@ -11,6 +11,7 @@ using System.Threading.Tasks;
using Newtonsoft.Json; using Newtonsoft.Json;
using Squidex.Areas.Api.Controllers.Rules.Models; using Squidex.Areas.Api.Controllers.Rules.Models;
using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Hosting;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.MongoDb;
@ -24,6 +25,8 @@ namespace Squidex.Config.Domain
private readonly IJsonSerializer jsonSerializer; private readonly IJsonSerializer jsonSerializer;
private readonly RuleRegistry ruleRegistry; private readonly RuleRegistry ruleRegistry;
public int Order => -1;
public SerializationInitializer(JsonSerializer jsonNetSerializer, IJsonSerializer jsonSerializer, RuleRegistry ruleRegistry) public SerializationInitializer(JsonSerializer jsonNetSerializer, IJsonSerializer jsonSerializer, RuleRegistry ruleRegistry)
{ {
this.jsonNetSerializer = jsonNetSerializer; this.jsonNetSerializer = jsonNetSerializer;

6
backend/src/Squidex/Config/Orleans/Helper.cs

@ -10,7 +10,7 @@ using System.Linq;
using System.Net; using System.Net;
using System.Net.Sockets; using System.Net.Sockets;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Infrastructure; using Squidex.Hosting.Configuration;
using Squidex.Infrastructure.Net; using Squidex.Infrastructure.Net;
namespace Squidex.Config.Orleans namespace Squidex.Config.Orleans
@ -37,7 +37,9 @@ namespace Squidex.Config.Orleans
if (chosen == null) if (chosen == null)
{ {
throw new ConfigurationException($"Hostname {addressOrHost} with family {family} is not a valid IP address or DNS name"); var error = new ConfigurationError($"Hostname {addressOrHost} with family {family} is not a valid IP address or DNS name");
throw new ConfigurationException(error);
} }
return chosen; return chosen;

38
backend/src/Squidex/Config/Startup/BackgroundHost.cs

@ -1,38 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Squidex.Infrastructure;
using Squidex.Log;
namespace Squidex.Config.Startup
{
public sealed class BackgroundHost : SafeHostedService
{
private readonly IEnumerable<IBackgroundProcess> targets;
public BackgroundHost(IEnumerable<IBackgroundProcess> targets, ISemanticLog log)
: base(log)
{
this.targets = targets;
}
protected override async Task StartAsync(ISemanticLog log, CancellationToken ct)
{
foreach (var target in targets.Distinct())
{
await target.StartAsync(ct);
log.LogInformation(w => w.WriteProperty("backgroundSystem", target.ToString()));
}
}
}
}

37
backend/src/Squidex/Config/Startup/InitializerHost.cs

@ -1,37 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Squidex.Infrastructure;
using Squidex.Log;
namespace Squidex.Config.Startup
{
public sealed class InitializerHost : SafeHostedService
{
private readonly IEnumerable<IInitializable> targets;
public InitializerHost(IEnumerable<IInitializable> targets, ISemanticLog log)
: base(log)
{
this.targets = targets;
}
protected override async Task StartAsync(ISemanticLog log, CancellationToken ct)
{
foreach (var target in targets.Distinct().OrderBy(x => x.Order))
{
await target.InitializeAsync(ct);
log.LogInformation(w => w.WriteProperty("initializedSystem", target.ToString()));
}
}
}
}

16
backend/src/Squidex/Config/Startup/LogConfigurationHost.cs

@ -11,21 +11,24 @@ using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Squidex.Log; using Squidex.Log;
namespace Squidex.Config.Startup namespace Squidex.Config.Startup
{ {
public sealed class LogConfigurationHost : SafeHostedService public sealed class LogConfigurationHost : IHostedService
{ {
private readonly IConfiguration configuration; private readonly IConfiguration configuration;
private readonly ISemanticLog log;
public LogConfigurationHost(ISemanticLog log, IConfiguration configuration) public LogConfigurationHost(IConfiguration configuration, ISemanticLog log)
: base(log)
{ {
this.configuration = configuration; this.configuration = configuration;
this.log = log;
} }
protected override Task StartAsync(ISemanticLog log, CancellationToken ct) public Task StartAsync(CancellationToken cancellationToken)
{ {
log.LogInformation(w => w log.LogInformation(w => w
.WriteProperty("message", "Application started") .WriteProperty("message", "Application started")
@ -46,5 +49,10 @@ namespace Squidex.Config.Startup
return Task.CompletedTask; return Task.CompletedTask;
} }
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
} }
} }

16
backend/src/Squidex/Config/Startup/MigrationRebuilderHost.cs

@ -7,24 +7,28 @@
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Migrations; using Migrations;
using Squidex.Log;
namespace Squidex.Config.Startup namespace Squidex.Config.Startup
{ {
public sealed class MigrationRebuilderHost : SafeHostedService public sealed class MigrationRebuilderHost : IHostedService
{ {
private readonly RebuildRunner rebuildRunner; private readonly RebuildRunner rebuildRunner;
public MigrationRebuilderHost(RebuildRunner rebuildRunner, ISemanticLog log) public MigrationRebuilderHost(RebuildRunner rebuildRunner)
: base(log)
{ {
this.rebuildRunner = rebuildRunner; this.rebuildRunner = rebuildRunner;
} }
protected override Task StartAsync(ISemanticLog log, CancellationToken ct) public Task StartAsync(CancellationToken cancellationToken)
{ {
return rebuildRunner.RunAsync(ct); return rebuildRunner.RunAsync(cancellationToken);
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
} }
} }
} }

16
backend/src/Squidex/Config/Startup/MigratorHost.cs

@ -7,24 +7,28 @@
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Squidex.Infrastructure.Migrations; using Squidex.Infrastructure.Migrations;
using Squidex.Log;
namespace Squidex.Config.Startup namespace Squidex.Config.Startup
{ {
public sealed class MigratorHost : SafeHostedService public sealed class MigratorHost : IHostedService
{ {
private readonly Migrator migrator; private readonly Migrator migrator;
public MigratorHost(Migrator migrator, ISemanticLog log) public MigratorHost(Migrator migrator)
: base(log)
{ {
this.migrator = migrator; this.migrator = migrator;
} }
protected override Task StartAsync(ISemanticLog log, CancellationToken ct) public Task StartAsync(CancellationToken cancellationToken)
{ {
return migrator.MigrateAsync(ct); return migrator.MigrateAsync(cancellationToken);
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
} }
} }
} }

47
backend/src/Squidex/Config/Startup/SafeHostedService.cs

@ -1,47 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Squidex.Log;
namespace Squidex.Config.Startup
{
public abstract class SafeHostedService : IHostedService
{
private readonly ISemanticLog log;
private bool isStarted;
protected SafeHostedService(ISemanticLog log)
{
this.log = log;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
await StartAsync(log, cancellationToken);
isStarted = true;
}
public async Task StopAsync(CancellationToken cancellationToken)
{
if (isStarted)
{
await StopAsync(log, cancellationToken);
}
}
protected abstract Task StartAsync(ISemanticLog log, CancellationToken ct);
protected virtual Task StopAsync(ISemanticLog log, CancellationToken ct)
{
return Task.CompletedTask;
}
}
}

52
backend/src/Squidex/Config/Web/WebExtensions.cs

@ -8,20 +8,15 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json;
using Squidex.Pipeline.Robots; using Squidex.Pipeline.Robots;
using Squidex.Web;
using Squidex.Web.Pipeline; using Squidex.Web.Pipeline;
namespace Squidex.Config.Web namespace Squidex.Config.Web
@ -130,52 +125,5 @@ namespace Squidex.Config.Web
.AllowAnyMethod() .AllowAnyMethod()
.AllowAnyHeader()); .AllowAnyHeader());
} }
public static void UseSquidexForwardingRules(this IApplicationBuilder app, IConfiguration config)
{
var urlsOptions = app.ApplicationServices.GetRequiredService<IOptions<UrlsOptions>>().Value;
if (urlsOptions.EnableForwardHeaders)
{
var options = new ForwardedHeadersOptions
{
AllowedHosts = new List<string>
{
new Uri(urlsOptions.BaseUrl).Host
},
ForwardedHeaders = ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost,
ForwardLimit = null,
RequireHeaderSymmetry = false
};
options.KnownNetworks.Clear();
options.KnownProxies.Clear();
if (urlsOptions.KnownProxies != null)
{
foreach (var proxy in urlsOptions.KnownProxies)
{
if (IPAddress.TryParse(proxy, out var address))
{
options.KnownProxies.Add(address);
}
}
}
app.UseForwardedHeaders(options);
}
app.UseMiddleware<CleanupHostMiddleware>();
if (urlsOptions.EnforceHost)
{
app.UseHostFiltering();
}
if (urlsOptions.EnforceHTTPS)
{
app.UseHttpsRedirection();
}
}
} }
} }

28
backend/src/Squidex/Config/Web/WebServices.cs

@ -5,8 +5,6 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Infrastructure;
@ -35,6 +33,9 @@ namespace Squidex.Config.Web
T.Setup(translator); T.Setup(translator);
services.AddDefaultWebServices(config);
services.AddDefaultForwardRules();
services.AddSingletonAs(c => new ExposedValues(c.GetRequiredService<IOptions<ExposedConfiguration>>().Value, config, typeof(WebServices).Assembly)) services.AddSingletonAs(c => new ExposedValues(c.GetRequiredService<IOptions<ExposedConfiguration>>().Value, config, typeof(WebServices).Assembly))
.AsSelf(); .AsSelf();
@ -102,29 +103,6 @@ namespace Squidex.Config.Web
.AddRazorRuntimeCompilation() .AddRazorRuntimeCompilation()
.AddSquidexPlugins(config) .AddSquidexPlugins(config)
.AddSquidexSerializers(); .AddSquidexSerializers();
var urlsOptions = config.GetSection("urls").Get<UrlsOptions>();
var host = urlsOptions.BuildHost();
if (urlsOptions.EnforceHost)
{
services.AddHostFiltering(options =>
{
options.AllowEmptyHosts = true;
options.AllowedHosts.Add(host.Host);
options.IncludeFailureMessage = false;
});
}
if (urlsOptions.EnforceHTTPS && !string.Equals(host.Host, "localhost", StringComparison.OrdinalIgnoreCase))
{
services.AddHttpsRedirection(options =>
{
options.HttpsPort = urlsOptions.HttpsPort;
});
}
} }
} }
} }

8
backend/src/Squidex/Program.cs

@ -9,7 +9,6 @@ using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Squidex.Areas.IdentityServer.Config;
using Squidex.Config.Domain; using Squidex.Config.Domain;
using Squidex.Config.Orleans; using Squidex.Config.Orleans;
using Squidex.Config.Startup; using Squidex.Config.Startup;
@ -39,10 +38,7 @@ namespace Squidex
services.AddHostedService<LogConfigurationHost>(); services.AddHostedService<LogConfigurationHost>();
// Step 1: Initialize all services. // Step 1: Initialize all services.
services.AddHostedService<InitializerHost>(); services.AddInitializer();
// Step 2: Create admin user.
services.AddHostedService<CreateAdminHost>();
}) })
.UseOrleans((context, builder) => .UseOrleans((context, builder) =>
{ {
@ -58,7 +54,7 @@ namespace Squidex
services.AddHostedService<MigrationRebuilderHost>(); services.AddHostedService<MigrationRebuilderHost>();
// Step 6: Start background processes. // Step 6: Start background processes.
services.AddHostedService<BackgroundHost>(); services.AddBackgroundProcesses();
}) })
.ConfigureWebHostDefaults(builder => .ConfigureWebHostDefaults(builder =>
{ {

11
backend/src/Squidex/Squidex.csproj

@ -44,27 +44,28 @@
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="5.0.1" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="5.0.1" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="5.0.0" /> <PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="5.0.0" />
<PackageReference Include="Microsoft.Data.Edm" Version="5.8.4" /> <PackageReference Include="Microsoft.Data.Edm" Version="5.8.4" />
<PackageReference Include="Microsoft.OData.Core" Version="7.7.3" /> <PackageReference Include="Microsoft.OData.Core" Version="7.8.1" />
<PackageReference Include="Microsoft.Orleans.Core" Version="3.3.0" /> <PackageReference Include="Microsoft.Orleans.Core" Version="3.3.0" />
<PackageReference Include="Microsoft.Orleans.Core.Abstractions" Version="3.3.0" /> <PackageReference Include="Microsoft.Orleans.Core.Abstractions" Version="3.3.0" />
<PackageReference Include="Microsoft.Orleans.OrleansRuntime" Version="3.3.0" /> <PackageReference Include="Microsoft.Orleans.OrleansRuntime" Version="3.3.0" />
<PackageReference Include="MongoDB.Driver" Version="2.11.5" /> <PackageReference Include="MongoDB.Driver" Version="2.11.5" />
<PackageReference Include="Namotion.Reflection" Version="1.0.15" /> <PackageReference Include="Namotion.Reflection" Version="1.0.15" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="NJsonSchema" Version="10.3.1" /> <PackageReference Include="NJsonSchema" Version="10.3.2" />
<PackageReference Include="NSwag.AspNetCore" Version="13.9.4" /> <PackageReference Include="NSwag.AspNetCore" Version="13.9.4" />
<PackageReference Include="OpenCover" Version="4.7.922" PrivateAssets="all" /> <PackageReference Include="OpenCover" Version="4.7.922" PrivateAssets="all" />
<PackageReference Include="Orleans.Providers.MongoDB" Version="3.2.0" /> <PackageReference Include="Orleans.Providers.MongoDB" Version="3.2.0" />
<PackageReference Include="OrleansDashboard" Version="3.1.0" /> <PackageReference Include="OrleansDashboard" Version="3.1.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="ReportGenerator" Version="4.8.2" PrivateAssets="all" /> <PackageReference Include="ReportGenerator" Version="4.8.3" PrivateAssets="all" />
<PackageReference Include="Squidex.Assets.Azure" Version="1.3.0" /> <PackageReference Include="Squidex.Assets.Azure" Version="1.3.0" />
<PackageReference Include="Squidex.Assets.GoogleCloud" Version="1.3.0" /> <PackageReference Include="Squidex.Assets.GoogleCloud" Version="1.3.0" />
<PackageReference Include="Squidex.Assets.FTP" Version="1.3.0" /> <PackageReference Include="Squidex.Assets.FTP" Version="1.3.0" />
<PackageReference Include="Squidex.Assets.Mongo" Version="1.3.0" /> <PackageReference Include="Squidex.Assets.Mongo" Version="1.3.0" />
<PackageReference Include="Squidex.Assets.S3" Version="1.3.0" /> <PackageReference Include="Squidex.Assets.S3" Version="1.3.0" />
<PackageReference Include="Squidex.Caching.Orleans" Version="1.1.0" /> <PackageReference Include="Squidex.Caching.Orleans" Version="1.3.0" />
<PackageReference Include="Squidex.ClientLibrary" Version="6.6.0" /> <PackageReference Include="Squidex.ClientLibrary" Version="6.8.0" />
<PackageReference Include="Squidex.Hosting" Version="1.7.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Linq" Version="4.3.0" /> <PackageReference Include="System.Linq" Version="4.3.0" />
<PackageReference Include="System.Runtime" Version="4.3.1" /> <PackageReference Include="System.Runtime" Version="4.3.1" />

3
backend/src/Squidex/Startup.cs

@ -74,7 +74,8 @@ namespace Squidex
{ {
app.UseCookiePolicy(); app.UseCookiePolicy();
app.UseSquidexForwardingRules(config); app.UseDefaultForwardRules();
app.UseSquidexHealthCheck(); app.UseSquidexHealthCheck();
app.UseSquidexRobotsTxt(); app.UseSquidexRobotsTxt();
app.UseSquidexTracking(); app.UseSquidexTracking();

4
backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientsTests.cs

@ -35,11 +35,11 @@ namespace Squidex.Domain.Apps.Core.Model.Apps
} }
[Fact] [Fact]
public void Should_throw_exception_if_assigning_client_with_same_id() public void Should_do_nothing_if_assigning_client_with_same_id()
{ {
var clients_1 = clients_0.Add("2", "my-secret"); var clients_1 = clients_0.Add("2", "my-secret");
Assert.Throws<ArgumentException>(() => clients_1.Add("2", "my-secret")); Assert.Same(clients_0, clients_1);
} }
[Fact] [Fact]

75
backend/tests/Squidex.Web.Tests/Pipeline/CleanupHostMiddlewareTests.cs

@ -1,75 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Xunit;
#pragma warning disable RECS0092 // Convert field to readonly
namespace Squidex.Web.Pipeline
{
public class CleanupHostMiddlewareTests
{
private readonly CleanupHostMiddleware sut;
private bool isNextCalled;
public CleanupHostMiddlewareTests()
{
Task Next(HttpContext context)
{
isNextCalled = true;
return Task.CompletedTask;
}
sut = new CleanupHostMiddleware(Next);
}
[Fact]
public async Task Should_cleanup_host_if_https_schema_contains_default_port()
{
var httpContext = new DefaultHttpContext();
httpContext.Request.Scheme = "https";
httpContext.Request.Host = new HostString("host", 443);
await sut.InvokeAsync(httpContext);
Assert.Null(httpContext.Request.Host.Port);
Assert.True(isNextCalled);
}
[Fact]
public async Task Should_cleanup_host_if_http_schema_contains_default_port()
{
var httpContext = new DefaultHttpContext();
httpContext.Request.Scheme = "http";
httpContext.Request.Host = new HostString("host", 80);
await sut.InvokeAsync(httpContext);
Assert.Null(httpContext.Request.Host.Port);
Assert.True(isNextCalled);
}
[Fact]
public async Task Should_not_cleanup_host_if_http_schema_contains_other_port()
{
var httpContext = new DefaultHttpContext();
httpContext.Request.Scheme = "http";
httpContext.Request.Host = new HostString("host", 8080);
await sut.InvokeAsync(httpContext);
Assert.Equal(8080, httpContext.Request.Host.Port);
Assert.True(isNextCalled);
}
}
}

65
backend/tests/Squidex.Web.Tests/UrlsOptionsTests.cs

@ -1,65 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Xunit;
namespace Squidex.Web
{
public sealed class UrlsOptionsTests
{
private readonly UrlsOptions sut = new UrlsOptions
{
BaseUrl = "http://localhost"
};
[Theory]
[InlineData("/url")]
[InlineData("/url/")]
[InlineData("url")]
public void Should_build_url_with_leading_slash(string path)
{
var url = sut.BuildUrl(path);
Assert.Equal("http://localhost/url/", url);
}
[Theory]
[InlineData("/url")]
[InlineData("/url/")]
[InlineData("url")]
public void Should_build_url_without_leading_slash(string path)
{
var url = sut.BuildUrl(path, false);
Assert.Equal("http://localhost/url", url);
}
[Fact]
public void Should_allow_same_host()
{
Assert.True(sut.IsAllowedHost("http://localhost"));
}
[Fact]
public void Should_allow_https_port()
{
Assert.True(sut.IsAllowedHost("https://localhost"));
}
[Fact]
public void Should_not_allow_other_host()
{
Assert.False(sut.IsAllowedHost("https://other:5000"));
}
[Fact]
public void Should_not_allow_same_host_with_other_port()
{
Assert.False(sut.IsAllowedHost("https://localhost:3000"));
}
}
}
Loading…
Cancel
Save