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.AddSingletonAs<MemoryAssetStore>()
.As<IAssetStore>();
services.AddSingleton<IAssetStore, MemoryAssetStore>();
}
}
}

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

@ -13,11 +13,11 @@
<PackageReference Include="Confluent.Kafka" Version="1.5.3" />
<PackageReference Include="Confluent.SchemaRegistry.Serdes" Version="1.3.0" />
<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="Microsoft.ApplicationInsights.AspNetCore" Version="2.16.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="NodaTime" Version="3.0.3" />
<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)
{
services.AddSingletonAs<CompositeUniqueValidatorFactory>()
.As<IValidatorsFactory>();
services.AddSingleton<IValidatorsFactory, CompositeUniqueValidatorFactory>();
}
}
}

5
backend/src/Migrations/MigrationPath.cs

@ -18,7 +18,7 @@ namespace Migrations
{
public sealed class MigrationPath : IMigrationPath
{
private const int CurrentVersion = 23;
private const int CurrentVersion = 24;
private readonly IServiceProvider serviceProvider;
public MigrationPath(IServiceProvider serviceProvider)
@ -74,7 +74,8 @@ namespace Migrations
}
// Version 12: Introduce roles.
if (version < 12)
// Version 24: Improve a naming in the languages config.
if (version < 24)
{
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))
{
throw new ArgumentException("Id already exists.", nameof(id));
return this;
}
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
{
services.AddSingletonAs<THandler>()
.As<IRuleActionHandler>();
services.AddSingleton<IRuleActionHandler, THandler>();
services.AddSingleton(new RuleActionRegistration(typeof(TAction)));
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="Markdig" Version="0.22.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
<PackageReference Include="Microsoft.OData.Core" Version="7.7.3" />
<PackageReference Include="NJsonSchema" Version="10.3.1" />
<PackageReference Include="Microsoft.OData.Core" Version="7.8.1" />
<PackageReference Include="NJsonSchema" Version="10.3.2" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.Jint" Version="3.0.0-beta-0" />
<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.MongoDb.Contents.Operations;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Hosting;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Queries;

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

@ -7,22 +7,14 @@
using System;
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
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 Elasticsearch.Net;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Hosting;
using Squidex.Infrastructure;
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.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using NodaTime;
@ -26,14 +25,14 @@ using Squidex.Shared.Users;
namespace Squidex.Domain.Apps.Entities.History
{
public class NotifoService : IInitializable, IUserEventHandler
public class NotifoService : IUserEventHandler
{
private static readonly Duration MaxAge = Duration.FromHours(12);
private readonly NotifoOptions options;
private readonly IUrlGenerator urlGenerator;
private readonly IUserResolver userResolver;
private readonly IClock clock;
private INotifoClient? client;
private readonly INotifoClient? client;
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.clock = clock;
}
public Task InitializeAsync(CancellationToken ct = default)
{
if (options.IsConfigured())
if (options.Value.IsConfigured())
{
var builder =
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();
}
return Task.CompletedTask;
}
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.Orleans;
using Squidex.Infrastructure.States;
using Squidex.Infrastructure.Tasks;
using Squidex.Infrastructure.Translations;
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
{
@ -151,7 +153,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Runner
private void Process(State job, CancellationToken ct)
{
ProcessAsync(job, ct).Forget();
TaskExtensions.Forget(ProcessAsync(job, 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" />
</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="Equals.Fody" Version="4.0.1" PrivateAssets="all" />
<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.Client;
using Newtonsoft.Json;
using Squidex.Hosting;
using Index = Microsoft.Azure.Documents.Index;
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.Threading;
using System.Threading.Tasks;
using Squidex.Hosting;
using Squidex.Log;
namespace Squidex.Infrastructure.EventSourcing

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

@ -7,7 +7,7 @@
</PropertyGroup>
<ItemGroup>
<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="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<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 EventStore.ClientAPI;
using EventStore.ClientAPI.Exceptions;
using Squidex.Hosting;
using Squidex.Hosting.Configuration;
using Squidex.Infrastructure.Json;
using Squidex.Log;
@ -48,7 +50,9 @@ namespace Squidex.Infrastructure.EventSourcing
}
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();

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

@ -14,6 +14,7 @@ using System.Threading.Tasks;
using EventStore.ClientAPI;
using EventStore.ClientAPI.Exceptions;
using EventStore.ClientAPI.Projections;
using Squidex.Hosting.Configuration;
using Squidex.Text;
namespace Squidex.Infrastructure.EventSourcing
@ -102,7 +103,9 @@ namespace Squidex.Infrastructure.EventSourcing
}
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.Dataflow;
using MongoDB.Bson;
using MongoDB.Bson.Serialization;
using MongoDB.Driver;
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.Tasks;
using MongoDB.Driver;
using Squidex.Hosting;
using Squidex.Hosting.Configuration;
#pragma warning disable RECS0108 // Warns about static fields in generic types
@ -112,7 +114,9 @@ namespace Squidex.Infrastructure.MongoDb
}
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.Tasks;
using RabbitMQ.Client;
using Squidex.Hosting;
using Squidex.Hosting.Configuration;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Json;
@ -69,14 +71,18 @@ namespace Squidex.Infrastructure.CQRS.Events
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;
}
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.Tasks;
using Microsoft.Extensions.Options;
using Squidex.Hosting;
namespace Squidex.Infrastructure
{

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

@ -9,6 +9,7 @@ using System.Threading;
using System.Threading.Tasks;
using Orleans;
using Orleans.Runtime;
using Squidex.Hosting;
namespace Squidex.Infrastructure.Orleans
{
@ -17,6 +18,8 @@ namespace Squidex.Infrastructure.Orleans
private const int NumTries = 10;
private readonly IGrainFactory grainFactory;
public string Name => typeof(T).Name;
public GrainBootstrap(IGrainFactory 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="Microsoft.Extensions.Caching.Abstractions" Version="5.0.0" />
<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">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
@ -26,10 +26,11 @@
<PackageReference Include="Microsoft.Orleans.Core" Version="3.3.0" />
<PackageReference Include="Microsoft.Orleans.OrleansRuntime" Version="3.3.0" />
<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="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.Text" Version="1.5.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />

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

@ -7,26 +7,14 @@
using System;
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
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.
// ==========================================================================
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
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;
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
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.
// ==========================================================================
using Microsoft.Extensions.Options;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Infrastructure;
using IGenericUrlGenerator = Squidex.Hosting.IUrlGenerator;
namespace Squidex.Web.Services
{
public sealed class UrlGenerator : IUrlGenerator
{
private readonly IAssetFileStore assetFileStore;
private readonly UrlsOptions urlsOptions;
private readonly IGenericUrlGenerator urlGenerator;
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(urlsOptions, nameof(urlsOptions));
Guard.NotNull(urlGenerator, nameof(urlGenerator));
this.assetFileStore = assetFileStore;
this.urlsOptions = urlsOptions.Value;
this.urlGenerator = urlGenerator;
CanGenerateAssetSourceUrl = allowAssetSourceUrl;
}
@ -39,32 +39,32 @@ namespace Squidex.Web.Services
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)
{
return urlsOptions.BuildUrl($"app/{appId.Name}/settings", false);
return urlGenerator.BuildUrl($"app/{appId.Name}/settings", false);
}
public string AssetContentBase()
{
return urlsOptions.BuildUrl("api/assets/");
return urlGenerator.BuildUrl("api/assets/");
}
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)
{
return urlsOptions.BuildUrl($"api/assets/{appId.Name}/{assetId}");
return urlGenerator.BuildUrl($"api/assets/{appId.Name}/{assetId}");
}
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)
@ -74,92 +74,92 @@ namespace Squidex.Web.Services
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)
{
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)
{
return urlsOptions.BuildUrl($"app/{appId.Name}/settings/backups", false);
return urlGenerator.BuildUrl($"app/{appId.Name}/settings/backups", false);
}
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)
{
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)
{
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)
{
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)
{
return urlsOptions.BuildUrl($"app/{appId.Name}/settings/contributors", false);
return urlGenerator.BuildUrl($"app/{appId.Name}/settings/contributors", false);
}
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)
{
return urlsOptions.BuildUrl($"app/{appId.Name}/settings/languages", false);
return urlGenerator.BuildUrl($"app/{appId.Name}/settings/languages", false);
}
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)
{
return urlsOptions.BuildUrl($"app/{appId.Name}/settings/plans", false);
return urlGenerator.BuildUrl($"app/{appId.Name}/settings/plans", false);
}
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)
{
return urlsOptions.BuildUrl($"app/{appId.Name}/rules", false);
return urlGenerator.BuildUrl($"app/{appId.Name}/rules", false);
}
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)
{
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)
{
return urlsOptions.BuildUrl($"app/{appId.Name}/settings/workflows", false);
return urlGenerator.BuildUrl($"app/{appId.Name}/settings/workflows", false);
}
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 Microsoft.Extensions.Options;
using NSwag;
using NSwag.Generation.Processors;
using NSwag.Generation.Processors.Contexts;
using Squidex.Hosting;
using Squidex.Web;
namespace Squidex.Areas.Api.Config.OpenApi
@ -24,9 +24,9 @@ namespace Squidex.Areas.Api.Config.OpenApi
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)
{

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

@ -7,9 +7,9 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Options;
using NSwag;
using NSwag.Generation.Processors.Security;
using Squidex.Hosting;
using Squidex.Pipeline.OpenApi;
using Squidex.Web;
@ -17,19 +17,19 @@ namespace Squidex.Areas.Api.Config.OpenApi
{
public sealed class SecurityProcessor : SecurityDefinitionAppender
{
public SecurityProcessor(IOptions<UrlsOptions> urlOptions)
: base(Constants.SecurityDefinition, Enumerable.Empty<string>(), CreateOAuthSchema(urlOptions.Value))
public SecurityProcessor(IUrlGenerator urlGenerator)
: base(Constants.SecurityDefinition, Enumerable.Empty<string>(), CreateOAuthSchema(urlGenerator))
{
}
private static OpenApiSecurityScheme CreateOAuthSchema(UrlsOptions urlOptions)
private static OpenApiSecurityScheme CreateOAuthSchema(IUrlGenerator urlGenerator)
{
var security = new OpenApiSecurityScheme
{
Type = OpenApiSecuritySchemeType.OAuth2
};
var tokenUrl = urlOptions.BuildUrl($"{Constants.IdentityServerPrefix}/connect/token", false);
var tokenUrl = urlGenerator.BuildUrl($"{Constants.IdentityServerPrefix}/connect/token", false);
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 Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Squidex.Areas.Api.Controllers.Statistics.Models;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.Plans;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Hosting;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.UsageTracking;
@ -34,16 +34,16 @@ namespace Squidex.Areas.Api.Controllers.Statistics
private readonly IAppPlansProvider appPlansProvider;
private readonly IAssetUsageTracker assetStatsRepository;
private readonly IDataProtector dataProtector;
private readonly UrlsOptions urlsOptions;
private readonly IUrlGenerator urlGenerator;
public UsagesController(
ICommandBus commandBus,
IDataProtectionProvider dataProtection,
IApiUsageTracker usageTracker,
IAppLogStore appLogStore,
IAppPlansProvider appPlansProvider,
IAssetUsageTracker assetStatsRepository,
IDataProtectionProvider dataProtection,
IOptions<UrlsOptions> urlsOptions)
IUrlGenerator urlGenerator)
: base(commandBus)
{
this.usageTracker = usageTracker;
@ -51,7 +51,7 @@ namespace Squidex.Areas.Api.Controllers.Statistics
this.appLogStore = appLogStore;
this.appPlansProvider = appPlansProvider;
this.assetStatsRepository = assetStatsRepository;
this.urlsOptions = urlsOptions.Value;
this.urlGenerator = urlGenerator;
dataProtector = dataProtection.CreateProtector("LogToken");
}
@ -73,7 +73,7 @@ namespace Squidex.Areas.Api.Controllers.Statistics
{
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 };

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

@ -6,7 +6,6 @@
// ==========================================================================
using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.Translations;
using Squidex.Text.Translations;
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 Squidex.Areas.Api.Controllers.Translations.Models;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Translations;
using Squidex.Shared;
using Squidex.Text.Translations;
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.IdentityModel.Logging;
using Squidex.Config;
using Squidex.Config.Startup;
using Squidex.Domain.Users;
using Squidex.Hosting;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Security;
using Squidex.Log;
@ -24,20 +24,21 @@ using Squidex.Shared.Users;
namespace Squidex.Areas.IdentityServer.Config
{
public sealed class CreateAdminHost : SafeHostedService
public sealed class CreateAdminInitializer : IInitializable
{
private readonly IServiceProvider serviceProvider;
private readonly MyIdentityOptions identityOptions;
public CreateAdminHost(ISemanticLog log, IServiceProvider serviceProvider, IOptions<MyIdentityOptions> identityOptions)
: base(log)
public int Order => int.MaxValue;
public CreateAdminInitializer(IServiceProvider serviceProvider, IOptions<MyIdentityOptions> identityOptions)
{
this.serviceProvider = serviceProvider;
this.identityOptions = identityOptions.Value;
}
protected override async Task StartAsync(ISemanticLog log, CancellationToken ct)
public async Task InitializeAsync(CancellationToken ct)
{
IdentityModelEventSource.ShowPII = identityOptions.ShowPII;
@ -91,6 +92,8 @@ namespace Squidex.Areas.IdentityServer.Config
}
catch (Exception ex)
{
var log = serviceProvider.GetRequiredService<ISemanticLog>();
log.LogError(ex, w => w
.WriteProperty("action", "createAdmin")
.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.Identity;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Squidex.Domain.Users;
using Squidex.Shared.Identity;
using Squidex.Web;
@ -27,15 +26,13 @@ namespace Squidex.Areas.IdentityServer.Config
{
public static void AddSquidexIdentityServer(this IServiceCollection services)
{
services.AddSingletonAs<IConfigureOptions<KeyManagementOptions>>(s =>
services.Configure<KeyManagementOptions>((c, options) =>
{
return new ConfigureOptions<KeyManagementOptions>(options =>
{
options.XmlRepository = s.GetRequiredService<IXmlRepository>();
});
options.XmlRepository = c.GetRequiredService<IXmlRepository>();
});
services.AddDataProtection().SetApplicationName("Squidex");
services.AddDataProtection()
.SetApplicationName("Squidex");
services.AddIdentity<IdentityUser, IdentityRole>()
.AddDefaultTokenProviders();
@ -61,6 +58,9 @@ namespace Squidex.Areas.IdentityServer.Config
services.AddSingletonAs<InMemoryResourcesStore>()
.As<IResourceStore>();
services.AddSingletonAs<CreateAdminInitializer>()
.AsSelf();
services.AddIdentityServer(options =>
{
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.Entities;
using Squidex.Domain.Users;
using Squidex.Hosting;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Security;
using Squidex.Shared;
@ -30,25 +31,15 @@ namespace Squidex.Areas.IdentityServer.Config
public class LazyClientStore : IClientStore
{
private readonly IServiceProvider serviceProvider;
private readonly IAppProvider appProvider;
private readonly Dictionary<string, Client> staticClients = new Dictionary<string, Client>(StringComparer.OrdinalIgnoreCase);
public LazyClientStore(
IServiceProvider serviceProvider,
IOptions<UrlsOptions> urlsOptions,
IOptions<MyIdentityOptions> identityOptions,
IAppProvider appProvider)
public LazyClientStore(IServiceProvider serviceProvider)
{
Guard.NotNull(appProvider, nameof(appProvider));
Guard.NotNull(identityOptions, nameof(identityOptions));
Guard.NotNull(serviceProvider, nameof(serviceProvider));
Guard.NotNull(urlsOptions, nameof(urlsOptions));
this.serviceProvider = serviceProvider;
this.appProvider = appProvider;
CreateStaticClients(urlsOptions, identityOptions);
CreateStaticClients();
}
public async Task<Client?> FindClientByIdAsync(string clientId)
@ -62,6 +53,8 @@ namespace Squidex.Areas.IdentityServer.Config
var (appName, appClientId) = clientId.GetClientParts();
var appProvider = serviceProvider.GetRequiredService<IAppProvider>();
if (!string.IsNullOrWhiteSpace(appName) && !string.IsNullOrWhiteSpace(appClientId))
{
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;
}
}
private static IEnumerable<Client> CreateStaticClients(UrlsOptions urlsOptions, MyIdentityOptions identityOptions)
private static IEnumerable<Client> CreateStaticClients(IUrlGenerator urlGenerator, MyIdentityOptions identityOptions)
{
var frontendId = Constants.FrontendClient;
@ -154,13 +151,13 @@ namespace Squidex.Areas.IdentityServer.Config
ClientName = frontendId,
RedirectUris = new List<string>
{
urlsOptions.BuildUrl("login;"),
urlsOptions.BuildUrl("client-callback-silent", false),
urlsOptions.BuildUrl("client-callback-popup", false)
urlGenerator.BuildUrl("login;"),
urlGenerator.BuildUrl("client-callback-silent", false),
urlGenerator.BuildUrl("client-callback-popup", false)
},
PostLogoutRedirectUris = new List<string>
{
urlsOptions.BuildUrl("logout", false)
urlGenerator.BuildUrl("logout", false)
},
AllowAccessTokensViaBrowser = true,
AllowedGrantTypes = GrantTypes.Implicit,
@ -189,8 +186,8 @@ namespace Squidex.Areas.IdentityServer.Config
},
RedirectUris = new List<string>
{
urlsOptions.BuildUrl($"{Constants.PortalPrefix}/signin-internal", false),
urlsOptions.BuildUrl($"{Constants.OrleansPrefix}/signin-internal", false)
urlGenerator.BuildUrl($"{Constants.PortalPrefix}/signin-internal", false),
urlGenerator.BuildUrl($"{Constants.OrleansPrefix}/signin-internal", false)
},
AccessTokenLifetime = (int)TimeSpan.FromDays(30).TotalSeconds,
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 Squidex.Config;
using Squidex.Domain.Users;
using Squidex.Hosting;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Security;
using Squidex.Infrastructure.Translations;
@ -41,7 +42,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account
private readonly UserManager<IdentityUser> userManager;
private readonly IUserFactory userFactory;
private readonly IUserEvents userEvents;
private readonly UrlsOptions urlsOptions;
private readonly IUrlGenerator urlGenerator;
private readonly MyIdentityOptions identityOptions;
private readonly ISemanticLog log;
private readonly IIdentityServerInteractionService interactions;
@ -51,7 +52,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account
UserManager<IdentityUser> userManager,
IUserFactory userFactory,
IUserEvents userEvents,
IOptions<UrlsOptions> urlsOptions,
IUrlGenerator urlGenerator,
IOptions<MyIdentityOptions> identityOptions,
ISemanticLog log,
IIdentityServerInteractionService interactions)
@ -59,7 +60,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account
this.identityOptions = identityOptions.Value;
this.interactions = interactions;
this.signInManager = signInManager;
this.urlsOptions = urlsOptions.Value;
this.urlGenerator = urlGenerator;
this.userEvents = userEvents;
this.userFactory = userFactory;
this.userManager = userManager;
@ -406,7 +407,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account
private IActionResult RedirectToReturnUrl(string? returnUrl)
{
if (urlsOptions.IsAllowedHost(returnUrl) || interactions.IsValidReturnUrl(returnUrl))
if (urlGenerator.IsAllowedHost(returnUrl) || interactions.IsValidReturnUrl(returnUrl))
{
return Redirect(returnUrl);
}

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

@ -5,13 +5,17 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using IdentityServer4;
using IdentityServer4.AccessTokenValidation;
using IdentityServer4.Hosting.LocalApiAuthentication;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Hosting;
using Squidex.Web;
namespace Squidex.Config.Authentication
@ -20,25 +24,14 @@ namespace Squidex.Config.Authentication
{
public static AuthenticationBuilder AddSquidexIdentityServerAuthentication(this AuthenticationBuilder authBuilder, MyIdentityOptions identityOptions, IConfiguration config)
{
var apiAuthorityUrl = 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;
var useCustomAuthorityUrl = !string.IsNullOrWhiteSpace(identityOptions.AuthorityUrl);
if (useCustomAuthorityUrl)
{
authBuilder.AddIdentityServerAuthentication(options =>
{
options.Authority = apiAuthorityUrl;
options.ApiName = apiScope;
options.Authority = identityOptions.AuthorityUrl;
options.ApiName = Constants.ApiScope;
options.ApiSecret = null;
options.RequireHttpsMetadata = identityOptions.RequiresHttps;
options.SupportedTokens = SupportedTokens.Jwt;
@ -46,19 +39,27 @@ namespace Squidex.Config.Authentication
}
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.ClientSecret = Constants.InternalClientSecret;
options.CallbackPath = "/signin-internal";
@ -85,5 +86,12 @@ namespace Squidex.Config.Authentication
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)
{
services.Configure<MyIdentityOptions>(
config.GetSection("identity"));
services.Configure<MyIdentityOptions>(config, "identity");
services.AddSingletonAs<DefaultUserResolver>()
.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.History;
using Squidex.Domain.Apps.Entities.Search;
using Squidex.Hosting;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
@ -28,8 +29,7 @@ namespace Squidex.Config.Domain
{
public static void AddSquidexAssets(this IServiceCollection services, IConfiguration config)
{
services.Configure<AssetOptions>(
config.GetSection("assets"));
services.Configure<AssetOptions>(config, "assets");
if (config.GetValue<bool>("assets:deleteRecursive"))
{
@ -162,7 +162,7 @@ namespace Squidex.Config.Domain
.As<IAssetThumbnailGenerator>();
services.AddSingletonAs(c => new DelegateInitializer(
c.GetRequiredService<IAssetStore>().GetType().FullName!,
c.GetRequiredService<IAssetStore>().GetType().Name,
c.GetRequiredService<IAssetStore>().InitializeAsync))
.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)
{
services.Configure<ReadonlyOptions>(
config.GetSection("mode"));
services.Configure<ReadonlyOptions>(config, "mode");
services.AddSingletonAs<InMemoryCommandBus>()
.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)
{
services.Configure<ContentOptions>(
config.GetSection("contents"));
services.Configure<ContentOptions>(config, "contents");
services.AddSingletonAs(c => new Lazy<IContentQueryService>(c.GetRequiredService<IContentQueryService>))
.AsSelf();

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

@ -8,7 +8,7 @@
using System;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Infrastructure;
using Squidex.Hosting.Configuration;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Json;
@ -27,7 +27,9 @@ namespace Squidex.Config.Domain
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");
@ -40,14 +42,18 @@ namespace Squidex.Config.Domain
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");
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}";
@ -60,7 +66,9 @@ namespace Squidex.Config.Domain
}
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)
{
services.Configure<GCHealthCheckOptions>(
config.GetSection("healthz:gc"));
services.Configure<GCHealthCheckOptions>(config, "healthz:gc");
services.AddHealthChecks()
.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.EventSourcing.Grains;
using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.Translations;
using Squidex.Infrastructure.UsageTracking;
using Squidex.Pipeline.Robots;
using Squidex.Text.Translations;
@ -43,12 +42,9 @@ namespace Squidex.Config.Domain
{
public static void AddSquidexInfrastructure(this IServiceCollection services, IConfiguration config)
{
services.Configure<UrlsOptions>(
config.GetSection("urls"));
services.Configure<ExposedConfiguration>(
config.GetSection("exposedConfiguration"));
services.Configure<ReplicatedCacheOptions>(
config.GetSection("caching:replicated"));
services.Configure<ExposedConfiguration>(config, "exposedConfiguration");
services.Configure<ReplicatedCacheOptions>(config, "caching:replicated");
services.AddReplicatedCache();
services.AddAsyncLocalCache();
@ -103,8 +99,7 @@ namespace Squidex.Config.Domain
public static void AddSquidexUsageTracking(this IServiceCollection services, IConfiguration config)
{
services.Configure<UsageOptions>(
config.GetSection("usage"));
services.Configure<UsageOptions>(config, "usage");
services.AddSingletonAs(c => new CachingUsageTracker(
c.GetRequiredService<BackgroundUsageTracker>(),
@ -123,12 +118,11 @@ namespace Squidex.Config.Domain
public static void AddSquidexTranslation(this IServiceCollection services, IConfiguration config)
{
services.Configure<DeepLOptions>(
config.GetSection("translations:deepL"));
services.Configure<GoogleCloudTranslationOptions>(
config.GetSection("translations:googleCloud"));
services.Configure<LanguagesOptions>(
config.GetSection("languages"));
services.Configure<GoogleCloudTranslationOptions>(config, "translations:googleCloud");
services.Configure<DeepLOptions>(config, "translations:deepL");
services.Configure<LanguagesOptions>(config, "languages");
services.AddSingletonAs<LanguagesInitializer>()
.AsSelf();
@ -145,14 +139,13 @@ namespace Squidex.Config.Domain
public static void AddSquidexControllerServices(this IServiceCollection services, IConfiguration config)
{
services.Configure<RobotsTxtOptions>(
config.GetSection("robots"));
services.Configure<CachingOptions>(
config.GetSection("caching"));
services.Configure<MyUIOptions>(
config.GetSection("ui"));
services.Configure<MyNewsOptions>(
config.GetSection("news"));
services.Configure<RobotsTxtOptions>(config, "robots");
services.Configure<CachingOptions>(config, "caching");
services.Configure<MyUIOptions>(config, "ui");
services.Configure<MyNewsOptions>(config, "news");
services.AddSingletonAs<FeaturesService>()
.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)
{
builder.ClearProviders();
builder.ConfigureSemanticLog(config);
builder.AddConfiguration(config.GetSection("logging"));
@ -34,38 +35,9 @@ namespace Squidex.Config.Domain
private static void AddServices(this IServiceCollection services, IConfiguration config)
{
services.Configure<RequestLogOptions>(
config.GetSection("logging"));
services.Configure<RequestLogOptions>(config, "logging");
services.Configure<RequestLogStoreOptions>(
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.Configure<RequestLogStoreOptions>(config, "logging");
services.AddSingletonAs(_ => new ApplicationInfoLogAppender(typeof(LoggingServices).Assembly, Guid.NewGuid()))
.As<ILogAppender>();
@ -73,15 +45,6 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<ActionContextLogAppender>()
.As<ILogAppender>();
services.AddSingletonAs<TimestampLogAppender>()
.As<ILogAppender>();
services.AddSingletonAs<DebugLogChannel>()
.As<ILogChannel>();
services.AddSingletonAs<SemanticLog>()
.As<ISemanticLog>();
services.AddSingletonAs<DefaultAppLogStore>()
.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)
{
services.Configure<RebuildOptions>(
config.GetSection("rebuild"));
services.Configure<RebuildOptions>(config, "rebuild");
services.AddSingletonAs<Migrator>()
.AsSelf();

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

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

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

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

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

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

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

@ -10,7 +10,7 @@ using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading.Tasks;
using Squidex.Infrastructure;
using Squidex.Hosting.Configuration;
using Squidex.Infrastructure.Net;
namespace Squidex.Config.Orleans
@ -37,7 +37,9 @@ namespace Squidex.Config.Orleans
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;

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.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Squidex.Log;
namespace Squidex.Config.Startup
{
public sealed class LogConfigurationHost : SafeHostedService
public sealed class LogConfigurationHost : IHostedService
{
private readonly IConfiguration configuration;
private readonly ISemanticLog log;
public LogConfigurationHost(ISemanticLog log, IConfiguration configuration)
: base(log)
public LogConfigurationHost(IConfiguration configuration, ISemanticLog log)
{
this.configuration = configuration;
this.log = log;
}
protected override Task StartAsync(ISemanticLog log, CancellationToken ct)
public Task StartAsync(CancellationToken cancellationToken)
{
log.LogInformation(w => w
.WriteProperty("message", "Application started")
@ -46,5 +49,10 @@ namespace Squidex.Config.Startup
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.Tasks;
using Microsoft.Extensions.Hosting;
using Migrations;
using Squidex.Log;
namespace Squidex.Config.Startup
{
public sealed class MigrationRebuilderHost : SafeHostedService
public sealed class MigrationRebuilderHost : IHostedService
{
private readonly RebuildRunner rebuildRunner;
public MigrationRebuilderHost(RebuildRunner rebuildRunner, ISemanticLog log)
: base(log)
public MigrationRebuilderHost(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.Tasks;
using Microsoft.Extensions.Hosting;
using Squidex.Infrastructure.Migrations;
using Squidex.Log;
namespace Squidex.Config.Startup
{
public sealed class MigratorHost : SafeHostedService
public sealed class MigratorHost : IHostedService
{
private readonly Migrator migrator;
public MigratorHost(Migrator migrator, ISemanticLog log)
: base(log)
public MigratorHost(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.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;
using Squidex.Infrastructure.Json;
using Squidex.Pipeline.Robots;
using Squidex.Web;
using Squidex.Web.Pipeline;
namespace Squidex.Config.Web
@ -130,52 +125,5 @@ namespace Squidex.Config.Web
.AllowAnyMethod()
.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.
// ==========================================================================
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Infrastructure;
@ -35,6 +33,9 @@ namespace Squidex.Config.Web
T.Setup(translator);
services.AddDefaultWebServices(config);
services.AddDefaultForwardRules();
services.AddSingletonAs(c => new ExposedValues(c.GetRequiredService<IOptions<ExposedConfiguration>>().Value, config, typeof(WebServices).Assembly))
.AsSelf();
@ -102,29 +103,6 @@ namespace Squidex.Config.Web
.AddRazorRuntimeCompilation()
.AddSquidexPlugins(config)
.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.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Squidex.Areas.IdentityServer.Config;
using Squidex.Config.Domain;
using Squidex.Config.Orleans;
using Squidex.Config.Startup;
@ -39,10 +38,7 @@ namespace Squidex
services.AddHostedService<LogConfigurationHost>();
// Step 1: Initialize all services.
services.AddHostedService<InitializerHost>();
// Step 2: Create admin user.
services.AddHostedService<CreateAdminHost>();
services.AddInitializer();
})
.UseOrleans((context, builder) =>
{
@ -58,7 +54,7 @@ namespace Squidex
services.AddHostedService<MigrationRebuilderHost>();
// Step 6: Start background processes.
services.AddHostedService<BackgroundHost>();
services.AddBackgroundProcesses();
})
.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.Bcl.AsyncInterfaces" Version="5.0.0" />
<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.Abstractions" Version="3.3.0" />
<PackageReference Include="Microsoft.Orleans.OrleansRuntime" Version="3.3.0" />
<PackageReference Include="MongoDB.Driver" Version="2.11.5" />
<PackageReference Include="Namotion.Reflection" Version="1.0.15" />
<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="OpenCover" Version="4.7.922" PrivateAssets="all" />
<PackageReference Include="Orleans.Providers.MongoDB" Version="3.2.0" />
<PackageReference Include="OrleansDashboard" Version="3.1.0" />
<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.GoogleCloud" 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.S3" Version="1.3.0" />
<PackageReference Include="Squidex.Caching.Orleans" Version="1.1.0" />
<PackageReference Include="Squidex.ClientLibrary" Version="6.6.0" />
<PackageReference Include="Squidex.Caching.Orleans" Version="1.3.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="System.Linq" Version="4.3.0" />
<PackageReference Include="System.Runtime" Version="4.3.1" />

3
backend/src/Squidex/Startup.cs

@ -74,7 +74,8 @@ namespace Squidex
{
app.UseCookiePolicy();
app.UseSquidexForwardingRules(config);
app.UseDefaultForwardRules();
app.UseSquidexHealthCheck();
app.UseSquidexRobotsTxt();
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]
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");
Assert.Throws<ArgumentException>(() => clients_1.Add("2", "my-secret"));
Assert.Same(clients_0, clients_1);
}
[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