diff --git a/Dockerfile b/Dockerfile index d61d83392..4becc2b89 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,6 +4,10 @@ FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS backend +# FFMPEG for tests +RUN apt-get update \ + && apt-get install -y ffmpeg + ARG SQUIDEX__BUILD__VERSION=7.0.0 WORKDIR /src @@ -73,7 +77,7 @@ ARG SQUIDEX__RUNTIME__VERSION=7.0.0 # Curl for debugging and libc-dev for protobuf RUN apt-get update \ - && apt-get install -y curl libc-dev + && apt-get install -y curl libc-dev ffmpeg # Default tool directory WORKDIR /tools diff --git a/backend/src/Migrations/Migrations/RebuildApps.cs b/backend/src/Migrations/Migrations/RebuildApps.cs index bba2571b8..c4f02fdfa 100644 --- a/backend/src/Migrations/Migrations/RebuildApps.cs +++ b/backend/src/Migrations/Migrations/RebuildApps.cs @@ -16,11 +16,9 @@ public sealed class RebuildApps( IOptions rebuildOptions) : IMigration { - private readonly RebuildOptions rebuildOptions = rebuildOptions.Value; - public Task UpdateAsync( CancellationToken ct) { - return rebuilder.RebuildAppsAsync(rebuildOptions.BatchSize, ct); + return rebuilder.RebuildAppsAsync(rebuildOptions.Value.CalculateBatchSize(), ct); } } diff --git a/backend/src/Migrations/Migrations/RebuildAssetFolders.cs b/backend/src/Migrations/Migrations/RebuildAssetFolders.cs index d0ed20058..1d4f53ad8 100644 --- a/backend/src/Migrations/Migrations/RebuildAssetFolders.cs +++ b/backend/src/Migrations/Migrations/RebuildAssetFolders.cs @@ -16,11 +16,9 @@ public sealed class RebuildAssetFolders( IOptions rebuildOptions) : IMigration { - private readonly RebuildOptions rebuildOptions = rebuildOptions.Value; - public Task UpdateAsync( CancellationToken ct) { - return rebuilder.RebuildAssetFoldersAsync(rebuildOptions.BatchSize, ct); + return rebuilder.RebuildAssetFoldersAsync(rebuildOptions.Value.CalculateBatchSize(), ct); } } diff --git a/backend/src/Migrations/Migrations/RebuildAssets.cs b/backend/src/Migrations/Migrations/RebuildAssets.cs index bfeea0e6b..0c39ca47c 100644 --- a/backend/src/Migrations/Migrations/RebuildAssets.cs +++ b/backend/src/Migrations/Migrations/RebuildAssets.cs @@ -16,11 +16,9 @@ public sealed class RebuildAssets( IOptions rebuildOptions) : IMigration { - private readonly RebuildOptions rebuildOptions = rebuildOptions.Value; - public Task UpdateAsync( CancellationToken ct) { - return rebuilder.RebuildAssetsAsync(rebuildOptions.BatchSize, ct); + return rebuilder.RebuildAssetsAsync(rebuildOptions.Value.CalculateBatchSize(), ct); } } diff --git a/backend/src/Migrations/Migrations/RebuildContents.cs b/backend/src/Migrations/Migrations/RebuildContents.cs index 6cbc23401..2de4ef43c 100644 --- a/backend/src/Migrations/Migrations/RebuildContents.cs +++ b/backend/src/Migrations/Migrations/RebuildContents.cs @@ -16,11 +16,9 @@ public sealed class RebuildContents( IOptions rebuildOptions) : IMigration { - private readonly RebuildOptions rebuildOptions = rebuildOptions.Value; - public Task UpdateAsync( CancellationToken ct) { - return rebuilder.RebuildContentAsync(rebuildOptions.BatchSize, ct); + return rebuilder.RebuildContentAsync(rebuildOptions.Value.CalculateBatchSize(), ct); } } diff --git a/backend/src/Migrations/Migrations/RebuildRules.cs b/backend/src/Migrations/Migrations/RebuildRules.cs index 545d48f22..25f9af47a 100644 --- a/backend/src/Migrations/Migrations/RebuildRules.cs +++ b/backend/src/Migrations/Migrations/RebuildRules.cs @@ -16,11 +16,9 @@ public sealed class RebuildRules( IOptions rebuildOptions) : IMigration { - private readonly RebuildOptions rebuildOptions = rebuildOptions.Value; - public Task UpdateAsync( CancellationToken ct) { - return rebuilder.RebuildRulesAsync(rebuildOptions.BatchSize, ct); + return rebuilder.RebuildRulesAsync(rebuildOptions.Value.CalculateBatchSize(), ct); } } diff --git a/backend/src/Migrations/Migrations/RebuildSchemas.cs b/backend/src/Migrations/Migrations/RebuildSchemas.cs index 4ed2b2265..9c6b80f32 100644 --- a/backend/src/Migrations/Migrations/RebuildSchemas.cs +++ b/backend/src/Migrations/Migrations/RebuildSchemas.cs @@ -16,11 +16,9 @@ public sealed class RebuildSchemas( IOptions rebuildOptions) : IMigration { - private readonly RebuildOptions rebuildOptions = rebuildOptions.Value; - public Task UpdateAsync( CancellationToken ct) { - return rebuilder.RebuildSchemasAsync(rebuildOptions.BatchSize, ct); + return rebuilder.RebuildSchemasAsync(rebuildOptions.Value.CalculateBatchSize(), ct); } } diff --git a/backend/src/Migrations/Migrations/RebuildSnapshots.cs b/backend/src/Migrations/Migrations/RebuildSnapshots.cs index f8842f528..2770c2d36 100644 --- a/backend/src/Migrations/Migrations/RebuildSnapshots.cs +++ b/backend/src/Migrations/Migrations/RebuildSnapshots.cs @@ -16,16 +16,17 @@ public sealed class RebuildSnapshots( IOptions rebuildOptions) : IMigration { - private readonly RebuildOptions rebuildOptions = rebuildOptions.Value; - public async Task UpdateAsync( CancellationToken ct) { - await rebuilder.RebuildAppsAsync(rebuildOptions.BatchSize, ct); - await rebuilder.RebuildSchemasAsync(rebuildOptions.BatchSize, ct); - await rebuilder.RebuildRulesAsync(rebuildOptions.BatchSize, ct); - await rebuilder.RebuildContentAsync(rebuildOptions.BatchSize, ct); - await rebuilder.RebuildAssetsAsync(rebuildOptions.BatchSize, ct); - await rebuilder.RebuildAssetFoldersAsync(rebuildOptions.BatchSize, ct); + var batchSize = rebuildOptions.Value.CalculateBatchSize(); + + await rebuilder.RebuildAppsAsync(batchSize, ct); + await rebuilder.RebuildSchemasAsync(batchSize, ct); + await rebuilder.RebuildRulesAsync(batchSize, ct); + await rebuilder.RebuildContentAsync(batchSize, ct); + await rebuilder.RebuildAssetsAsync(batchSize, ct); + await rebuilder.RebuildAssetFoldersAsync(batchSize, ct); + await rebuilder.RebuildTeamsAsync(batchSize, ct); } } diff --git a/backend/src/Migrations/RebuildOptions.cs b/backend/src/Migrations/RebuildOptions.cs index c43d09304..2ed515faa 100644 --- a/backend/src/Migrations/RebuildOptions.cs +++ b/backend/src/Migrations/RebuildOptions.cs @@ -23,6 +23,8 @@ public sealed class RebuildOptions public bool Schemas { get; set; } + public bool Teams { get; set; } + public int BatchSize { get; set; } = 100; public int CalculateBatchSize() diff --git a/backend/src/Migrations/RebuildRunner.cs b/backend/src/Migrations/RebuildRunner.cs index b66cd95e5..7720dc500 100644 --- a/backend/src/Migrations/RebuildRunner.cs +++ b/backend/src/Migrations/RebuildRunner.cs @@ -16,40 +16,43 @@ public sealed class RebuildRunner( Rebuilder rebuilder, RebuildFiles rebuildFiles) { - private readonly RebuildOptions rebuildOptions = rebuildOptions.Value; - public async Task RunAsync( CancellationToken ct) { - var batchSize = rebuildOptions.CalculateBatchSize(); + var batchSize = rebuildOptions.Value.CalculateBatchSize(); - if (rebuildOptions.Apps) + if (rebuildOptions.Value.Apps) { await rebuilder.RebuildAppsAsync(batchSize, ct); } - if (rebuildOptions.Schemas) + if (rebuildOptions.Value.Teams) + { + await rebuilder.RebuildTeamsAsync(batchSize, ct); + } + + if (rebuildOptions.Value.Schemas) { await rebuilder.RebuildSchemasAsync(batchSize, ct); } - if (rebuildOptions.Rules) + if (rebuildOptions.Value.Rules) { await rebuilder.RebuildRulesAsync(batchSize, ct); } - if (rebuildOptions.Assets) + if (rebuildOptions.Value.Assets) { await rebuilder.RebuildAssetsAsync(batchSize, ct); await rebuilder.RebuildAssetFoldersAsync(batchSize, ct); } - if (rebuildOptions.AssetFiles) + if (rebuildOptions.Value.AssetFiles) { await rebuildFiles.RepairAsync(ct); } - if (rebuildOptions.Contents) + if (rebuildOptions.Value.Contents) { await rebuilder.RebuildContentAsync(batchSize, ct); } diff --git a/backend/src/Migrations/RebuilderExtensions.cs b/backend/src/Migrations/RebuilderExtensions.cs index 580c20123..e1b4b44e4 100644 --- a/backend/src/Migrations/RebuilderExtensions.cs +++ b/backend/src/Migrations/RebuilderExtensions.cs @@ -10,11 +10,13 @@ using Squidex.Domain.Apps.Core.Assets; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Core.Teams; using Squidex.Domain.Apps.Entities.Apps.DomainObject; using Squidex.Domain.Apps.Entities.Assets.DomainObject; using Squidex.Domain.Apps.Entities.Contents.DomainObject; using Squidex.Domain.Apps.Entities.Rules.DomainObject; using Squidex.Domain.Apps.Entities.Schemas.DomainObject; +using Squidex.Domain.Apps.Entities.Teams.DomainObject; using Squidex.Events; using Squidex.Infrastructure.Commands; @@ -71,4 +73,12 @@ public static class RebuilderExtensions return rebuilder.RebuildAsync(streamFilter, batchSize, AllowedErrorRate, ct); } + + public static Task RebuildTeamsAsync(this Rebuilder rebuilder, int batchSize, + CancellationToken ct = default) + { + var streamFilter = StreamFilter.Prefix("team-"); + + return rebuilder.RebuildAsync(streamFilter, batchSize, AllowedErrorRate, ct); + } } diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/MySql/Migrations/.editorconfig b/backend/src/Squidex.Data.EntityFramework/Providers/MySql/Migrations/.editorconfig new file mode 100644 index 000000000..1e79455e5 --- /dev/null +++ b/backend/src/Squidex.Data.EntityFramework/Providers/MySql/Migrations/.editorconfig @@ -0,0 +1,9 @@ +[*.cs] +# MA0007: Add a comma after the last value +dotnet_diagnostic.MA0007.severity = none + +# MA0048: File name must match type name +dotnet_diagnostic.MA0048.severity = none + +# SA1633: File must have header +dotnet_diagnostic.SA1633.severity = none \ No newline at end of file diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/Migrations/.editorconfig b/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/Migrations/.editorconfig new file mode 100644 index 000000000..1e79455e5 --- /dev/null +++ b/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/Migrations/.editorconfig @@ -0,0 +1,9 @@ +[*.cs] +# MA0007: Add a comma after the last value +dotnet_diagnostic.MA0007.severity = none + +# MA0048: File name must match type name +dotnet_diagnostic.MA0048.severity = none + +# SA1633: File must have header +dotnet_diagnostic.SA1633.severity = none \ No newline at end of file diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/Migrations/.editorconfig b/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/Migrations/.editorconfig new file mode 100644 index 000000000..1e79455e5 --- /dev/null +++ b/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/Migrations/.editorconfig @@ -0,0 +1,9 @@ +[*.cs] +# MA0007: Add a comma after the last value +dotnet_diagnostic.MA0007.severity = none + +# MA0048: File name must match type name +dotnet_diagnostic.MA0048.severity = none + +# SA1633: File must have header +dotnet_diagnostic.SA1633.severity = none \ No newline at end of file diff --git a/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/MongoContentRepository_SnapshotStore.cs b/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/MongoContentRepository_SnapshotStore.cs index dbd78a5ce..bf1bbdcf5 100644 --- a/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/MongoContentRepository_SnapshotStore.cs +++ b/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/MongoContentRepository_SnapshotStore.cs @@ -108,8 +108,6 @@ public partial class MongoContentRepository : ISnapshotStore, IDel async Task ISnapshotStore.WriteManyAsync(IEnumerable> jobs, CancellationToken ct) { - var validJobs = jobs.Where(x => IsValid(x.Value)).ToList(); - using (Telemetry.Activities.StartActivity("MongoContentRepository/WriteManyAsync")) { var collectionUpdates = new Dictionary, List>(); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/FFMpegAssetMetadataSource.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/FFMpegAssetMetadataSource.cs new file mode 100644 index 000000000..342d76082 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/FFMpegAssetMetadataSource.cs @@ -0,0 +1,77 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using FFMpegCore; +using Squidex.Domain.Apps.Core.Assets; +using Squidex.Domain.Apps.Entities.Assets.Commands; + +namespace Squidex.Domain.Apps.Entities.Assets; + +public sealed class FFMpegAssetMetadataSource : IAssetMetadataSource +{ + public async Task EnhanceAsync(UploadAssetCommand command, + CancellationToken ct) + { + if (command.Type != AssetType.Unknown) + { + return; + } + + try + { + var analysis = await FFProbe.AnalyseAsync(command.File.OpenRead(), cancellationToken: ct); + + void TryAddInt(string name, long? value) + { + if (value > 0) + { + command.Metadata[name] = value.Value; + } + } + + void TryAddTimeSpan(string name, TimeSpan value) + { + if (value != TimeSpan.Zero) + { + command.Metadata[name] = value.ToString(); + } + } + + var audioStream = analysis.AudioStreams.FirstOrDefault(); + if (audioStream != null) + { + TryAddTimeSpan(KnownMetadataKeys.Duration, audioStream.Duration); + + TryAddInt(KnownMetadataKeys.AudioBitrate, audioStream.BitRate / 1000); + TryAddInt(KnownMetadataKeys.AudioSampleRate, audioStream.SampleRateHz); + + command.Type = AssetType.Audio; + } + + var videoStream = analysis.VideoStreams.FirstOrDefault(); + if (videoStream != null) + { + TryAddTimeSpan(KnownMetadataKeys.Duration, videoStream.Duration); + + TryAddInt(KnownMetadataKeys.VideoWidth, videoStream.Width); + TryAddInt(KnownMetadataKeys.VideoHeight, videoStream.Height); + + command.Type = AssetType.Video; + } + } + catch (Exception) + { + // Throws for invalid file types. + return; + } + } + + public IEnumerable Format(Asset asset) + { + yield break; + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/FileTagAssetMetadataSource.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/FileTagAssetMetadataSource.cs index e559deb8e..de4b0ae07 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/FileTagAssetMetadataSource.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/FileTagAssetMetadataSource.cs @@ -45,90 +45,89 @@ public sealed class FileTagAssetMetadataSource : IAssetMetadataSource { try { - using (var file = Create(new FileAbstraction(command.File), ReadStyle.Average)) + using var file = Create(new FileAbstraction(command.File), ReadStyle.Average); + + if (file.Properties == null) { - if (file.Properties == null) - { - return Task.CompletedTask; - } + return Task.CompletedTask; + } - var type = file.Properties.MediaTypes; + var type = file.Properties.MediaTypes; - if (type == MediaTypes.Audio) - { - command.Type = AssetType.Audio; - } - else if (type == MediaTypes.Photo) - { - command.Type = AssetType.Image; - } - else if (type.HasFlag(MediaTypes.Video)) - { - command.Type = AssetType.Video; - } + if (type == MediaTypes.Audio) + { + command.Type = AssetType.Audio; + } + else if (type == MediaTypes.Photo) + { + command.Type = AssetType.Image; + } + else if (type.HasFlag(MediaTypes.Video)) + { + command.Type = AssetType.Video; + } - var pw = file.Properties.PhotoWidth; - var ph = file.Properties.PhotoHeight; + var pw = file.Properties.PhotoWidth; + var ph = file.Properties.PhotoHeight; - if (pw > 0 && ph > 0) - { - command.Metadata[KnownMetadataKeys.PixelWidth] = pw; - command.Metadata[KnownMetadataKeys.PixelHeight] = ph; - } + if (pw > 0 && ph > 0) + { + command.Metadata[KnownMetadataKeys.PixelWidth] = pw; + command.Metadata[KnownMetadataKeys.PixelHeight] = ph; + } - void TryAddString(string name, string? value) + void TryAddString(string name, string? value) + { + if (!string.IsNullOrWhiteSpace(value)) { - if (!string.IsNullOrWhiteSpace(value)) - { - command.Metadata.Add(name, value); - } + command.Metadata.Add(name, value); } + } - void TryAddInt(string name, int? value) + void TryAddInt(string name, long? value) + { + if (value > 0) { - if (value > 0) - { - command.Metadata.Add(name, (double)value.Value); - } + command.Metadata[name] = value.Value; } + } - void TryAddDouble(string name, double? value) + void TryAddDouble(string name, double? value) + { + if (value > 0) { - if (value > 0) - { - command.Metadata.Add(name, value.Value); - } + command.Metadata[name] = value.Value; } + } - void TryAddTimeSpan(string name, TimeSpan value) + void TryAddTimeSpan(string name, TimeSpan value) + { + if (value != TimeSpan.Zero) { - if (value != TimeSpan.Zero) - { - command.Metadata.Add(name, value.ToString()); - } + command.Metadata[name] = value.ToString(); } + } - if (file.Tag is ImageTag imageTag) - { - TryAddDouble(KnownMetadataKeys.Latitude, imageTag.Latitude); - TryAddDouble(KnownMetadataKeys.Longitude, imageTag.Longitude); + if (file.Tag is ImageTag imageTag) + { + TryAddDouble(KnownMetadataKeys.Latitude, imageTag.Latitude); + TryAddDouble(KnownMetadataKeys.Longitude, imageTag.Longitude); - TryAddString(KnownMetadataKeys.Created, imageTag.DateTime?.ToIso8601()); - } + TryAddString(KnownMetadataKeys.Created, imageTag.DateTime?.ToIso8601()); + } - TryAddTimeSpan(KnownMetadataKeys.Duration, file.Properties.Duration); + TryAddTimeSpan(KnownMetadataKeys.Duration, file.Properties.Duration); - TryAddInt(KnownMetadataKeys.BitsPerSample, file.Properties.BitsPerSample); - TryAddInt(KnownMetadataKeys.AudioBitrate, file.Properties.AudioBitrate); - TryAddInt(KnownMetadataKeys.AudioChannels, file.Properties.AudioChannels); - TryAddInt(KnownMetadataKeys.AudioSampleRate, file.Properties.AudioSampleRate); - TryAddInt(KnownMetadataKeys.ImageQuality, file.Properties.PhotoQuality); + TryAddInt(KnownMetadataKeys.BitsPerSample, file.Properties.BitsPerSample); + TryAddInt(KnownMetadataKeys.AudioBitrate, file.Properties.AudioBitrate); + TryAddInt(KnownMetadataKeys.AudioChannels, file.Properties.AudioChannels); + TryAddInt(KnownMetadataKeys.AudioSampleRate, file.Properties.AudioSampleRate); + TryAddInt(KnownMetadataKeys.ImageQuality, file.Properties.PhotoQuality); - TryAddInt(KnownMetadataKeys.VideoWidth, file.Properties.VideoWidth); - TryAddInt(KnownMetadataKeys.VideoHeight, file.Properties.VideoHeight); + TryAddInt(KnownMetadataKeys.VideoWidth, file.Properties.VideoWidth); + TryAddInt(KnownMetadataKeys.VideoHeight, file.Properties.VideoHeight); - TryAddString(KnownMetadataKeys.Description, file.Properties.Description); - } + TryAddString(KnownMetadataKeys.Description, file.Properties.Description); return Task.CompletedTask; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj b/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj index 76e539ab1..18cf701c0 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj +++ b/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj @@ -25,6 +25,7 @@ + diff --git a/backend/src/Squidex.Web/Constants.cs b/backend/src/Squidex.Web/Constants.cs index 678ed295b..8a738b540 100644 --- a/backend/src/Squidex.Web/Constants.cs +++ b/backend/src/Squidex.Web/Constants.cs @@ -16,6 +16,8 @@ public static class Constants public const string ApiSecurityScheme = "API"; + public const string ExternalScheme = "ExternalOidc"; + public const string PrefixApi = "/api"; public const string PrefixIdentityServer = "/identity-server"; diff --git a/backend/src/Squidex.Web/ContextExtensions.cs b/backend/src/Squidex.Web/ContextExtensions.cs index 0a9b6e32a..2611d95e1 100644 --- a/backend/src/Squidex.Web/ContextExtensions.cs +++ b/backend/src/Squidex.Web/ContextExtensions.cs @@ -5,13 +5,27 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; using RequestContext = Squidex.Domain.Apps.Entities.Context; namespace Squidex.Web; public static class ContextExtensions { + public static async Task HasSchemeAsync(this HttpContext httpContext, string name) + { + var provider = httpContext.RequestServices.GetService(); + if (provider == null) + { + return false; + } + + return await provider.GetSchemeAsync(name) != null; + } + public static RequestContext Context(this HttpContext httpContext) { var context = httpContext.Features.Get(); diff --git a/backend/src/Squidex.Web/Pipeline/AppResolver.cs b/backend/src/Squidex.Web/Pipeline/AppResolver.cs index 8e4502960..4e7f0b836 100644 --- a/backend/src/Squidex.Web/Pipeline/AppResolver.cs +++ b/backend/src/Squidex.Web/Pipeline/AppResolver.cs @@ -38,7 +38,6 @@ public sealed class AppResolver(IAppProvider appProvider) : IAsyncActionFilter var isFrontend = user.IsInClient(DefaultClients.Frontend); var app = await appProvider.GetAppAsync(appName, !isFrontend, context.HttpContext.RequestAborted); - if (app == null) { var log = context.HttpContext.RequestServices?.GetService>(); diff --git a/backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs b/backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs index ac7515e26..9dc70a71f 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs @@ -73,6 +73,10 @@ public static class IdentityServerServices }); services.AddOpenIddict() + .AddValidation(options => + { + options.EnableAuthorizationEntryValidation(); + }) .AddCore(builder => { builder.SetDefaultScopeEntity(); @@ -145,17 +149,14 @@ public static class IdentityServerServices services.Configure((c, options) => { - var urlGenerator = c.GetRequiredService(); - - var identityPrefix = Constants.PrefixIdentityServer; - var identityOptions = c.GetRequiredService>().Value; + var urlBuilder = c.GetRequiredService(); - Uri BuildUrl(string path) + static Uri BuildUrl(string path) { - return new Uri($"{identityPrefix.TrimStart('/')}/{path}", UriKind.Relative); + return new Uri($"{Constants.PrefixIdentityServer.TrimStart('/')}/{path}", UriKind.Relative); } - options.Issuer = new Uri(urlGenerator.BuildUrl()); + options.Issuer = new Uri(urlBuilder.BuildUrl()); options.AuthorizationEndpointUris.SetEndpoint( BuildUrl("connect/authorize")); diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs index 3c2c0b78b..2b8678756 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; @@ -16,6 +17,7 @@ using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Translations; using Squidex.Shared.Identity; using Squidex.Shared.Users; +using Squidex.Web; namespace Squidex.Areas.IdentityServer.Controllers.Account; @@ -109,20 +111,22 @@ public sealed class AccountController( [HttpGet] [Route("account/logout/")] - public async Task Logout() + public IActionResult Logout() { - await SignInManager.SignOutAsync(); - - return Redirect("~/../"); + return SignOut(new AuthenticationProperties + { + RedirectUri = Url.Content("~/../"), + }); } [HttpGet] [Route("account/logout-redirect/")] - public async Task LogoutRedirect() + public IActionResult LogoutRedirect() { - await SignInManager.SignOutAsync(); - - return RedirectToAction(nameof(LogoutCompleted)); + return SignOut(new AuthenticationProperties + { + RedirectUri = Url.Action(nameof(LogoutCompleted)), + }); } [HttpGet] diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Connect/AuthorizationController.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Connect/AuthorizationController.cs index 470028aff..c0bcaca15 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Controllers/Connect/AuthorizationController.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Connect/AuthorizationController.cs @@ -14,7 +14,6 @@ using Microsoft.IdentityModel.Tokens; using OpenIddict.Abstractions; using OpenIddict.Server.AspNetCore; using Squidex.Areas.IdentityServer.Config; -using Squidex.Areas.IdentityServer.Controllers; using Squidex.Domain.Users; using Squidex.Infrastructure; using Squidex.Shared.Identity; @@ -22,7 +21,7 @@ using Squidex.Shared.Users; using Squidex.Web; using static OpenIddict.Abstractions.OpenIddictConstants; -namespace Squidex.Areas.Account.Controllers.Connect; +namespace Squidex.Areas.IdentityServer.Controllers.Connect; public class AuthorizationController( IOpenIddictScopeManager scopeManager, @@ -31,7 +30,6 @@ public class AuthorizationController( : IdentityServerController { [HttpPost("connect/token")] - [Produces("application/json")] public async Task Exchange() { var request = HttpContext.GetOpenIddictServerRequest(); @@ -160,7 +158,14 @@ public class AuthorizationController( { await SignInManager.SignOutAsync(); - return SignOut(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + List schemes = [OpenIddictServerAspNetCoreDefaults.AuthenticationScheme]; + + if (await HttpContext.HasSchemeAsync(Constants.ExternalScheme)) + { + schemes.Add(Constants.ExternalScheme); + } + + return SignOut(schemes.ToArray()); } private async Task CreatePrincipalAsync(OpenIddictRequest request, IUser user) diff --git a/backend/src/Squidex/Config/Authentication/OidcHandler.cs b/backend/src/Squidex/Config/Authentication/OidcHandler.cs index 9d2461b34..aa8ecfa5b 100644 --- a/backend/src/Squidex/Config/Authentication/OidcHandler.cs +++ b/backend/src/Squidex/Config/Authentication/OidcHandler.cs @@ -34,11 +34,6 @@ public sealed class OidcHandler(MyIdentityOptions options) : OpenIdConnectEvents return base.TokenValidated(context); } - public override Task AuthenticationFailed(AuthenticationFailedContext context) - { - return base.AuthenticationFailed(context); - } - public override Task RedirectToIdentityProviderForSignOut(RedirectContext context) { if (!string.IsNullOrEmpty(options.OidcOnSignoutRedirectUrl)) diff --git a/backend/src/Squidex/Config/Authentication/OidcServices.cs b/backend/src/Squidex/Config/Authentication/OidcServices.cs index 908382a1f..30d6decea 100644 --- a/backend/src/Squidex/Config/Authentication/OidcServices.cs +++ b/backend/src/Squidex/Config/Authentication/OidcServices.cs @@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Squidex.Infrastructure; +using Squidex.Web; namespace Squidex.Config.Authentication; @@ -17,9 +18,11 @@ public static class OidcServices { if (identityOptions.IsOidcConfigured()) { - var displayName = !string.IsNullOrWhiteSpace(identityOptions.OidcName) ? identityOptions.OidcName : OpenIdConnectDefaults.DisplayName; + var displayName = !string.IsNullOrWhiteSpace(identityOptions.OidcName) ? + identityOptions.OidcName : + OpenIdConnectDefaults.DisplayName; - authBuilder.AddOpenIdConnect("ExternalOidc", displayName, options => + authBuilder.AddOpenIdConnect(Constants.ExternalScheme, displayName, options => { options.Events = new OidcHandler(identityOptions); options.Authority = identityOptions.OidcAuthority; diff --git a/backend/src/Squidex/Config/Domain/AssetServices.cs b/backend/src/Squidex/Config/Domain/AssetServices.cs index 1b586d267..d5ebca676 100644 --- a/backend/src/Squidex/Config/Domain/AssetServices.cs +++ b/backend/src/Squidex/Config/Domain/AssetServices.cs @@ -94,6 +94,9 @@ public static class AssetServices services.AddSingletonAs() .As(); + services.AddSingletonAs() + .As(); + services.AddAssetTus(); } diff --git a/backend/src/Squidex/appsettings.json b/backend/src/Squidex/appsettings.json index e0a8b0e40..115f1978d 100644 --- a/backend/src/Squidex/appsettings.json +++ b/backend/src/Squidex/appsettings.json @@ -666,9 +666,9 @@ "githubClient": "211ea00e726baf754c78", "githubSecret": "d0a0d0fe2c26469ae20987ac265b3a339fd73132", - // Settings for Microsoft auth (keep empty to disable).3 + // Settings for Microsoft auth (keep empty to disable). // - // NOTE: Tennant is optional for using a specific AzureAD tenant + // NOTE: Tenant is optional for using a specific AzureAD tenant "microsoftClient": "b55da740-6648-4502-8746-b9003f29d5f1", "microsoftSecret": "idWbANxNYEF4cB368WXJhjN", "microsoftTenant": null, @@ -774,7 +774,10 @@ "rules": false, // Set to true to rebuild schemas. - "schemas": false + "schemas": false, + + // Set to true to rebuild teams. + "teams": false }, // A list of configuration values that should be exposed from the info endpoint and in the UI. diff --git a/backend/tests/Squidex.Data.Tests.CodeGenerator/Squidex.Data.Tests.CodeGenerator.csproj b/backend/tests/Squidex.Data.Tests.CodeGenerator/Squidex.Data.Tests.CodeGenerator.csproj index 0a3f22da1..88aefa3bb 100644 --- a/backend/tests/Squidex.Data.Tests.CodeGenerator/Squidex.Data.Tests.CodeGenerator.csproj +++ b/backend/tests/Squidex.Data.Tests.CodeGenerator/Squidex.Data.Tests.CodeGenerator.csproj @@ -18,7 +18,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/Text/AtlasTextIndexTests.cs b/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/Text/AtlasTextIndexTests.cs index a83bb6212..b56770216 100644 --- a/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/Text/AtlasTextIndexTests.cs +++ b/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/Text/AtlasTextIndexTests.cs @@ -16,11 +16,9 @@ public class AtlasTextIndexTests(AtlasTextIndexFixture fixture) : TextIndexerTes public override bool SupportsGeo => true; - public AtlasTextIndexFixture _ { get; } = fixture; - public override Task CreateSutAsync() { - return Task.FromResult(_.Index); + return Task.FromResult(fixture.Index); } [Fact] diff --git a/backend/tests/Squidex.Data.Tests/MongoDb/Infrastructure/EventSourcing/MongoEventConsumerProcessorIntegrationTests_Direct.cs b/backend/tests/Squidex.Data.Tests/MongoDb/Infrastructure/EventSourcing/MongoEventConsumerProcessorIntegrationTests_Direct.cs index 25d79e847..984e352b8 100644 --- a/backend/tests/Squidex.Data.Tests/MongoDb/Infrastructure/EventSourcing/MongoEventConsumerProcessorIntegrationTests_Direct.cs +++ b/backend/tests/Squidex.Data.Tests/MongoDb/Infrastructure/EventSourcing/MongoEventConsumerProcessorIntegrationTests_Direct.cs @@ -12,10 +12,8 @@ namespace Squidex.MongoDb.Infrastructure.EventSourcing; [Trait("Category", "Dependencies")] public class MongoEventConsumerProcessorIntegrationTests_Direct(MongoEventStoreFixture_Direct fixture) : EventConsumerProcessorIntegrationTests, IClassFixture { - public MongoEventStoreFixture _ { get; } = fixture; - public override IEventStore CreateStore() { - return _.EventStore; + return fixture.EventStore; } } diff --git a/backend/tests/Squidex.Data.Tests/MongoDb/Infrastructure/EventSourcing/MongoEventStoreParallelInsertTests.cs b/backend/tests/Squidex.Data.Tests/MongoDb/Infrastructure/EventSourcing/MongoEventStoreParallelInsertTests.cs index 288e5c6d0..1d6e2e5f0 100644 --- a/backend/tests/Squidex.Data.Tests/MongoDb/Infrastructure/EventSourcing/MongoEventStoreParallelInsertTests.cs +++ b/backend/tests/Squidex.Data.Tests/MongoDb/Infrastructure/EventSourcing/MongoEventStoreParallelInsertTests.cs @@ -16,12 +16,13 @@ using Squidex.Infrastructure.TestHelpers; namespace Squidex.MongoDb.Infrastructure.EventSourcing; [Trait("Category", "Dependencies")] -public class MongoEventStoreParallelInsertTests : IClassFixture +public class MongoEventStoreParallelInsertTests(MongoEventStoreFixture_Replica fixture) : IClassFixture { private readonly TestState state = new TestState(DomainId.Empty); - private readonly DefaultEventFormatter eventFormatter; - - public MongoEventStoreFixture _ { get; } + private readonly DefaultEventFormatter eventFormatter = + new DefaultEventFormatter( + new TypeRegistry().Add("MyEvent"), + TestUtils.DefaultSerializer); public class MyEvent : IEvent { @@ -60,15 +61,6 @@ public class MongoEventStoreParallelInsertTests : IClassFixture("MyEvent"); - - eventFormatter = new DefaultEventFormatter(typeRegistry, TestUtils.DefaultSerializer); - } - [Fact] public async Task Should_insert_and_retrieve_parallel() { @@ -182,7 +174,7 @@ public class MongoEventStoreParallelInsertTests : IClassFixture>()); } @@ -206,7 +198,7 @@ public class MongoEventStoreParallelInsertTests : IClassFixture(new MyEvent()), commitId)); } - await _.EventStore.AppendAsync(commitId, streamName, EtagVersion.Any, commitList); + await fixture.EventStore.AppendAsync(commitId, streamName, EtagVersion.Any, commitList); } if (i < iterations - 1) diff --git a/backend/tests/Squidex.Data.Tests/Squidex.Data.Tests.csproj b/backend/tests/Squidex.Data.Tests/Squidex.Data.Tests.csproj index d57059643..9b674e998 100644 --- a/backend/tests/Squidex.Data.Tests/Squidex.Data.Tests.csproj +++ b/backend/tests/Squidex.Data.Tests/Squidex.Data.Tests.csproj @@ -6,8 +6,8 @@ latest enable enable - en true + SA0001;NETSDK1206 diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FFMpegAssetMetadataSourceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FFMpegAssetMetadataSourceTests.cs new file mode 100644 index 000000000..7955cd035 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FFMpegAssetMetadataSourceTests.cs @@ -0,0 +1,98 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Assets; +using Squidex.Domain.Apps.Core.Assets; +using Squidex.Domain.Apps.Entities.Assets.Commands; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Domain.Apps.Entities.Assets; + +public class FFMpegAssetMetadataSourceTests : GivenContext +{ + private readonly FFMpegAssetMetadataSource sut = new FFMpegAssetMetadataSource(); + + [Fact] + public async Task Should_ignore_files_with_type() + { + var command = FakeCommand(AssetType.Image); + + await sut.EnhanceAsync(command, default); + + Assert.Equal(AssetType.Image, command.Type); + } + + [Fact] + public async Task Should_not_set_image_height_and_width_metadata_when_file_does_not_have_those_values() + { + var command = Command("SampleAudio_0.4mb.mp3"); + + await sut.EnhanceAsync(command, default); + + Assert.Null(command.Metadata.GetInt32(KnownMetadataKeys.PixelWidth)); + Assert.Null(command.Metadata.GetInt32(KnownMetadataKeys.PixelHeight)); + } + + [Fact] + public async Task Should_provide_metadata_for_audio() + { + var command = Command("SampleAudio_0.4mb.mp3"); + + await sut.EnhanceAsync(command, default); + + Assert.Equal(AssetType.Audio, command.Type); + Assert.Equal(JsonValue.Create(128L), command.Metadata[KnownMetadataKeys.AudioBitrate]); + Assert.Equal(JsonValue.Create(44100L), command.Metadata[KnownMetadataKeys.AudioSampleRate]); + } + + [Fact] + public async Task Should_provide_metadata_for_broken_video() + { + var command = Command("SampleVideo_Broken.mp4"); + + await sut.EnhanceAsync(command, default); + + Assert.Equal(AssetType.Video, command.Type); + Assert.Equal(JsonValue.Create("00:00:11"), command.Metadata[KnownMetadataKeys.Duration]); + Assert.Equal(JsonValue.Create(317L), command.Metadata[KnownMetadataKeys.AudioBitrate]); + Assert.Equal(JsonValue.Create(48000L), command.Metadata[KnownMetadataKeys.AudioSampleRate]); + Assert.Equal(JsonValue.Create(1080L), command.Metadata[KnownMetadataKeys.VideoHeight]); + Assert.Equal(JsonValue.Create(1920L), command.Metadata[KnownMetadataKeys.VideoWidth]); + } + + [Fact] + public void Should_not_format_asset() + { + var source = CreateAsset() with + { + Type = AssetType.Image, + }; + + var formatted = sut.Format(source); + + Assert.Empty(formatted); + } + + private static UploadAssetCommand Command(string path) + { + var file = new FileInfo(Path.Combine("Assets", "TestFiles", path)); + + return new CreateAsset + { + File = new DelegateAssetFile(file.Name, "mime", file.Length, file.OpenRead), + }; + } + + private static UploadAssetCommand FakeCommand(AssetType type) + { + return new CreateAsset + { + Type = type, + }; + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/TestFiles/SampleVideo_Broken.mp4 b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/TestFiles/SampleVideo_Broken.mp4 new file mode 100644 index 000000000..0753b6158 Binary files /dev/null and b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/TestFiles/SampleVideo_Broken.mp4 differ diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/AzureTextIndexTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/AzureTextIndexTests.cs index 45004314b..a1a3e544f 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/AzureTextIndexTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/AzureTextIndexTests.cs @@ -12,11 +12,9 @@ public class AzureTextIndexTests(AzureTextIndexFixture fixture) : TextIndexerTes { public override bool SupportsGeo => true; - public AzureTextIndexFixture _ { get; } = fixture; - public override Task CreateSutAsync() { - return Task.FromResult(_.Index); + return Task.FromResult(fixture.Index); } [Fact] diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/ElasticSearchTextIndexTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/ElasticSearchTextIndexTests.cs index d743b51ae..016f37df4 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/ElasticSearchTextIndexTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/ElasticSearchTextIndexTests.cs @@ -12,11 +12,9 @@ public class ElasticSearchTextIndexTests(ElasticSearchTextIndexFixture fixture) { public override bool SupportsGeo => true; - public ElasticSearchTextIndexFixture _ { get; } = fixture; - public override Task CreateSutAsync() { - return Task.FromResult(_.Index); + return Task.FromResult(fixture.Index); } [Fact] diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/OpenSearchTextIndexTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/OpenSearchTextIndexTests.cs index 4c1c68681..481371619 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/OpenSearchTextIndexTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/OpenSearchTextIndexTests.cs @@ -12,11 +12,9 @@ public class OpenSearchTextIndexTests(OpenSearchTextIndexFixture fixture) : Text { public override bool SupportsGeo => true; - public OpenSearchTextIndexFixture _ { get; } = fixture; - public override Task CreateSutAsync() { - return Task.FromResult(_.Index); + return Task.FromResult(fixture.Index); } [Fact] diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj index a9ef7a7b6..b97c6f84a 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj @@ -63,6 +63,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest diff --git a/tools/TestSuite/TestSuite.ApiTests/AssetFormatTests.cs b/tools/TestSuite/TestSuite.ApiTests/AssetFormatTests.cs index db7fe8105..bcf3a64ad 100644 --- a/tools/TestSuite/TestSuite.ApiTests/AssetFormatTests.cs +++ b/tools/TestSuite/TestSuite.ApiTests/AssetFormatTests.cs @@ -200,9 +200,6 @@ public class AssetFormatTests(CreatedAppFixture fixture) : IClassFixture