Browse Source

A few things mixed together. (#959)

* A few things mixed together.

* Add missing files.
pull/961/head
Sebastian Stehle 3 years ago
committed by GitHub
parent
commit
08b90221b3
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 22
      backend/extensions/Squidex.Extensions/Actions/Discourse/DiscourseActionHandler.cs
  2. 2
      backend/extensions/Squidex.Extensions/Actions/Discourse/DiscoursePlugin.cs
  3. 15
      backend/extensions/Squidex.Extensions/Actions/Fastly/FastlyActionHandler.cs
  4. 6
      backend/extensions/Squidex.Extensions/Actions/Fastly/FastlyPlugin.cs
  5. 60
      backend/extensions/Squidex.Extensions/Actions/Medium/MediumActionHandler.cs
  6. 9
      backend/extensions/Squidex.Extensions/Actions/Medium/MediumPlugin.cs
  7. 13
      backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderActionHandler.cs
  8. 5
      backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderPlugin.cs
  9. 15
      backend/extensions/Squidex.Extensions/Actions/Slack/SlackActionHandler.cs
  10. 5
      backend/extensions/Squidex.Extensions/Actions/Slack/SlackPlugin.cs
  11. 9
      backend/extensions/Squidex.Extensions/Actions/Typesense/TypesenseActionHandler.cs
  12. 1
      backend/extensions/Squidex.Extensions/Actions/Typesense/TypesensePlugin.cs
  13. 8
      backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookActionHandler.cs
  14. 6
      backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookPlugin.cs
  15. 20
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/HttpJintExtension.cs
  16. 32
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/AtlasIndexDefinition.cs
  17. 6
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/AtlasTextIndex.cs
  18. 82
      backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/TemplatesClient.cs
  19. 19
      backend/src/Squidex.Domain.Apps.Entities/Backup/TempFolderBackupArchiveLocation.cs
  20. 4
      backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj
  21. 35
      backend/src/Squidex.Web/GraphQL/GraphQLRunner.cs
  22. 6
      backend/src/Squidex.Web/Squidex.Web.csproj
  23. 16
      backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsSharedController.cs
  24. 3
      backend/src/Squidex/Areas/Api/Controllers/UI/MyUIOptions.cs
  25. 1
      backend/src/Squidex/Areas/Api/Controllers/UI/UIController.cs
  26. 33
      backend/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs
  27. 2
      backend/src/Squidex/Config/Authentication/IdentityServices.cs
  28. 5
      backend/src/Squidex/Config/Domain/BackupsServices.cs
  29. 2
      backend/src/Squidex/Config/Domain/ContentsServices.cs
  30. 2
      backend/src/Squidex/Config/Domain/InfrastructureServices.cs
  31. 13
      backend/src/Squidex/Config/Domain/StoreServices.cs
  32. 3
      backend/src/Squidex/Config/Web/WebServices.cs
  33. 6
      backend/src/Squidex/Squidex.csproj
  34. 11
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryFixture.cs
  35. 19
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryFixture.cs
  36. 22
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/AtlasTextIndexFixture.cs
  37. 4
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj
  38. 11
      frontend/package-lock.json
  39. 1
      frontend/package.json
  40. 1
      frontend/src/app/shell/declarations.ts
  41. 3
      frontend/src/app/shell/module.ts
  42. 5
      frontend/src/app/shell/pages/internal/feedback-menu.component.html
  43. 2
      frontend/src/app/shell/pages/internal/feedback-menu.component.scss
  44. 44
      frontend/src/app/shell/pages/internal/feedback-menu.component.ts
  45. 1
      frontend/src/app/shell/pages/internal/internal-area.component.html
  46. 1050
      frontend/src/app/theme/icomoon/demo.html
  47. BIN
      frontend/src/app/theme/icomoon/fonts/icomoon.eot
  48. 1
      frontend/src/app/theme/icomoon/fonts/icomoon.svg
  49. BIN
      frontend/src/app/theme/icomoon/fonts/icomoon.ttf
  50. BIN
      frontend/src/app/theme/icomoon/fonts/icomoon.woff
  51. 2
      frontend/src/app/theme/icomoon/selection.json
  52. 13
      frontend/src/app/theme/icomoon/style.css

22
backend/extensions/Squidex.Extensions/Actions/Discourse/DiscourseActionHandler.cs

@ -68,19 +68,17 @@ public sealed class DiscourseActionHandler : RuleActionHandler<DiscourseAction,
protected override async Task<Result> ExecuteJobAsync(DiscourseJob job, protected override async Task<Result> ExecuteJobAsync(DiscourseJob job,
CancellationToken ct = default) CancellationToken ct = default)
{ {
using (var httpClient = httpClientFactory.CreateClient()) var httpClient = httpClientFactory.CreateClient("DiscourseAction");
var request = new HttpRequestMessage(HttpMethod.Post, job.RequestUrl)
{ {
using (var request = new HttpRequestMessage(HttpMethod.Post, job.RequestUrl) Content = new StringContent(job.RequestBody, Encoding.UTF8, "application/json")
{ };
Content = new StringContent(job.RequestBody, Encoding.UTF8, "application/json")
}) request.Headers.TryAddWithoutValidation("Api-Key", job.ApiKey);
{ request.Headers.TryAddWithoutValidation("Api-Username", job.ApiUserName);
request.Headers.TryAddWithoutValidation("Api-Key", job.ApiKey);
request.Headers.TryAddWithoutValidation("Api-Username", job.ApiUserName); return await httpClient.OneWayRequestAsync(request, job.RequestBody, ct);
return await httpClient.OneWayRequestAsync(request, job.RequestBody, ct);
}
}
} }
} }

2
backend/extensions/Squidex.Extensions/Actions/Discourse/DiscoursePlugin.cs

@ -15,6 +15,8 @@ public sealed class DiscoursePlugin : IPlugin
{ {
public void ConfigureServices(IServiceCollection services, IConfiguration config) public void ConfigureServices(IServiceCollection services, IConfiguration config)
{ {
services.AddHttpClient("DiscourseAction");
services.AddRuleAction<DiscourseAction, DiscourseActionHandler>(); services.AddRuleAction<DiscourseAction, DiscourseActionHandler>();
} }
} }

15
backend/extensions/Squidex.Extensions/Actions/Fastly/FastlyActionHandler.cs

@ -47,19 +47,14 @@ public sealed class FastlyActionHandler : RuleActionHandler<FastlyAction, Fastly
protected override async Task<Result> ExecuteJobAsync(FastlyJob job, protected override async Task<Result> ExecuteJobAsync(FastlyJob job,
CancellationToken ct = default) CancellationToken ct = default)
{ {
using (var httpClient = httpClientFactory.CreateClient()) var httpClient = httpClientFactory.CreateClient("FastlyAction");
{
httpClient.Timeout = TimeSpan.FromSeconds(2);
var requestUrl = $"https://api.fastly.com/service/{job.FastlyServiceID}/purge/{job.Key}"; var requestUrl = $"/service/{job.FastlyServiceID}/purge/{job.Key}";
var request = new HttpRequestMessage(HttpMethod.Post, requestUrl);
using (var request = new HttpRequestMessage(HttpMethod.Post, requestUrl)) request.Headers.Add("Fastly-Key", job.FastlyApiKey);
{
request.Headers.Add("Fastly-Key", job.FastlyApiKey);
return await httpClient.OneWayRequestAsync(request, ct: ct); return await httpClient.OneWayRequestAsync(request, ct: ct);
}
}
} }
} }

6
backend/extensions/Squidex.Extensions/Actions/Fastly/FastlyPlugin.cs

@ -15,6 +15,12 @@ public sealed class FastlyPlugin : IPlugin
{ {
public void ConfigureServices(IServiceCollection services, IConfiguration config) public void ConfigureServices(IServiceCollection services, IConfiguration config)
{ {
services.AddHttpClient("Fastly", options =>
{
options.BaseAddress = new Uri("https://api.fastly.com");
options.Timeout = TimeSpan.FromSeconds(2);
});
services.AddRuleAction<FastlyAction, FastlyActionHandler>(); services.AddRuleAction<FastlyAction, FastlyActionHandler>();
} }
} }

60
backend/extensions/Squidex.Extensions/Actions/Medium/MediumActionHandler.cs

@ -80,50 +80,44 @@ public sealed class MediumActionHandler : RuleActionHandler<MediumAction, Medium
protected override async Task<Result> ExecuteJobAsync(MediumJob job, protected override async Task<Result> ExecuteJobAsync(MediumJob job,
CancellationToken ct = default) CancellationToken ct = default)
{ {
using (var httpClient = httpClientFactory.CreateClient()) var httpClient = httpClientFactory.CreateClient("MediumAction");
{
httpClient.Timeout = TimeSpan.FromSeconds(4);
httpClient.DefaultRequestHeaders.Add("Accept", "application/json");
httpClient.DefaultRequestHeaders.Add("Accept-Charset", "utf-8");
httpClient.DefaultRequestHeaders.Add("User-Agent", "Squidex Headless CMS");
string path;
if (!string.IsNullOrWhiteSpace(job.PublicationId)) string path;
{
path = $"v1/publications/{job.PublicationId}/posts";
}
else
{
HttpResponseMessage response = null;
var meRequest = BuildMeRequest(job); if (!string.IsNullOrWhiteSpace(job.PublicationId))
try {
{ path = $"/v1/publications/{job.PublicationId}/posts";
response = await httpClient.SendAsync(meRequest, ct); }
else
{
HttpResponseMessage response = null;
var responseString = await response.Content.ReadAsStringAsync(ct); var meRequest = BuildGetRequest(job, "/v1/me");
var responseJson = serializer.Deserialize<UserResponse>(responseString); try
{
response = await httpClient.SendAsync(meRequest, ct);
var id = responseJson.Data?.Id; var responseString = await response.Content.ReadAsStringAsync(ct);
var responseJson = serializer.Deserialize<UserResponse>(responseString);
path = $"v1/users/{id}/posts"; var id = responseJson.Data?.Id;
}
catch (Exception ex)
{
var requestDump = DumpFormatter.BuildDump(meRequest, response, ex.ToString());
return Result.Failed(ex, requestDump); path = $"/v1/users/{id}/posts";
}
} }
catch (Exception ex)
{
var requestDump = DumpFormatter.BuildDump(meRequest, response, ex.ToString());
return await httpClient.OneWayRequestAsync(BuildPostRequest(job, path), job.RequestBody, ct); return Result.Failed(ex, requestDump);
}
} }
return await httpClient.OneWayRequestAsync(BuildPostRequest(job, path), job.RequestBody, ct);
} }
private static HttpRequestMessage BuildPostRequest(MediumJob job, string path) private static HttpRequestMessage BuildPostRequest(MediumJob job, string path)
{ {
var request = new HttpRequestMessage(HttpMethod.Post, $"https://api.medium.com/{path}") var request = new HttpRequestMessage(HttpMethod.Post, path)
{ {
Content = new StringContent(job.RequestBody, Encoding.UTF8, "application/json") Content = new StringContent(job.RequestBody, Encoding.UTF8, "application/json")
}; };
@ -133,9 +127,9 @@ public sealed class MediumActionHandler : RuleActionHandler<MediumAction, Medium
return request; return request;
} }
private static HttpRequestMessage BuildMeRequest(MediumJob job) private static HttpRequestMessage BuildGetRequest(MediumJob job, string path)
{ {
var request = new HttpRequestMessage(HttpMethod.Get, "https://api.medium.com/v1/me"); var request = new HttpRequestMessage(HttpMethod.Get, path);
request.Headers.Add("Authorization", $"Bearer {job.AccessToken}"); request.Headers.Add("Authorization", $"Bearer {job.AccessToken}");

9
backend/extensions/Squidex.Extensions/Actions/Medium/MediumPlugin.cs

@ -15,6 +15,15 @@ public sealed class MediumPlugin : IPlugin
{ {
public void ConfigureServices(IServiceCollection services, IConfiguration config) public void ConfigureServices(IServiceCollection services, IConfiguration config)
{ {
services.AddHttpClient("MediumAction", options =>
{
options.BaseAddress = new Uri("https://api.medium.com/");
options.Timeout = TimeSpan.FromSeconds(4);
options.DefaultRequestHeaders.Add("Accept", "application/json");
options.DefaultRequestHeaders.Add("Accept-Charset", "utf-8");
options.DefaultRequestHeaders.Add("User-Agent", "Squidex Headless CMS");
});
services.AddRuleAction<MediumAction, MediumActionHandler>(); services.AddRuleAction<MediumAction, MediumActionHandler>();
} }
} }

13
backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderActionHandler.cs

@ -36,15 +36,14 @@ public sealed class PrerenderActionHandler : RuleActionHandler<PrerenderAction,
protected override async Task<Result> ExecuteJobAsync(PrerenderJob job, protected override async Task<Result> ExecuteJobAsync(PrerenderJob job,
CancellationToken ct = default) CancellationToken ct = default)
{ {
using (var httpClient = httpClientFactory.CreateClient()) var httpClient = httpClientFactory.CreateClient("Prerender");
var request = new HttpRequestMessage(HttpMethod.Post, "/recache")
{ {
var request = new HttpRequestMessage(HttpMethod.Post, "https://api.prerender.io/recache") Content = new StringContent(job.RequestBody, Encoding.UTF8, "application/json")
{ };
Content = new StringContent(job.RequestBody, Encoding.UTF8, "application/json")
};
return await httpClient.OneWayRequestAsync(request, job.RequestBody, ct); return await httpClient.OneWayRequestAsync(request, job.RequestBody, ct);
}
} }
} }

5
backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderPlugin.cs

@ -15,6 +15,11 @@ public sealed class PrerenderPlugin : IPlugin
{ {
public void ConfigureServices(IServiceCollection services, IConfiguration config) public void ConfigureServices(IServiceCollection services, IConfiguration config)
{ {
services.AddHttpClient("PrerenderAction", options =>
{
options.BaseAddress = new Uri("https://api.prerender.io");
});
services.AddRuleAction<PrerenderAction, PrerenderActionHandler>(); services.AddRuleAction<PrerenderAction, PrerenderActionHandler>();
} }
} }

15
backend/extensions/Squidex.Extensions/Actions/Slack/SlackActionHandler.cs

@ -41,17 +41,14 @@ public sealed class SlackActionHandler : RuleActionHandler<SlackAction, SlackJob
protected override async Task<Result> ExecuteJobAsync(SlackJob job, protected override async Task<Result> ExecuteJobAsync(SlackJob job,
CancellationToken ct = default) CancellationToken ct = default)
{ {
using (var httpClient = httpClientFactory.CreateClient()) var httpClient = httpClientFactory.CreateClient("SlackAction");
{
httpClient.Timeout = TimeSpan.FromSeconds(2);
var request = new HttpRequestMessage(HttpMethod.Post, job.RequestUrl) var request = new HttpRequestMessage(HttpMethod.Post, job.RequestUrl)
{ {
Content = new StringContent(job.RequestBody, Encoding.UTF8, "application/json") Content = new StringContent(job.RequestBody, Encoding.UTF8, "application/json")
}; };
return await httpClient.OneWayRequestAsync(request, job.RequestBody, ct); return await httpClient.OneWayRequestAsync(request, job.RequestBody, ct);
}
} }
} }

5
backend/extensions/Squidex.Extensions/Actions/Slack/SlackPlugin.cs

@ -15,6 +15,11 @@ public sealed class SlackPlugin : IPlugin
{ {
public void ConfigureServices(IServiceCollection services, IConfiguration config) public void ConfigureServices(IServiceCollection services, IConfiguration config)
{ {
services.AddHttpClient("SlackAction", options =>
{
options.Timeout = TimeSpan.FromSeconds(2);
});
services.AddRuleAction<SlackAction, SlackActionHandler>(); services.AddRuleAction<SlackAction, SlackActionHandler>();
} }
} }

9
backend/extensions/Squidex.Extensions/Actions/Typesense/TypesenseActionHandler.cs

@ -109,7 +109,7 @@ public sealed class TypesenseActionHandler : RuleActionHandler<TypesenseAction,
return Result.Ignored(); return Result.Ignored();
} }
var httpClient = httpClientFactory.CreateClient(); var httpClient = httpClientFactory.CreateClient("TypesenseAction");
HttpRequestMessage request; HttpRequestMessage request;
@ -125,12 +125,9 @@ public sealed class TypesenseActionHandler : RuleActionHandler<TypesenseAction,
request = new HttpRequestMessage(HttpMethod.Delete, $"{job.ServerUrl}/{job.ContentId}"); request = new HttpRequestMessage(HttpMethod.Delete, $"{job.ServerUrl}/{job.ContentId}");
} }
using (request) request.Headers.TryAddWithoutValidation("X-Typesense-Api-Key", job.ServerKey);
{
request.Headers.TryAddWithoutValidation("X-Typesense-Api-Key", job.ServerKey);
return await httpClient.OneWayRequestAsync(request, job.Content, ct); return await httpClient.OneWayRequestAsync(request, job.Content, ct);
}
} }
} }

1
backend/extensions/Squidex.Extensions/Actions/Typesense/TypesensePlugin.cs

@ -15,6 +15,7 @@ public sealed class TypesensePlugin : IPlugin
{ {
public void ConfigureServices(IServiceCollection services, IConfiguration config) public void ConfigureServices(IServiceCollection services, IConfiguration config)
{ {
services.AddHttpClient("TypesenseAction");
services.AddRuleAction<TypesenseAction, TypesenseActionHandler>(); services.AddRuleAction<TypesenseAction, TypesenseActionHandler>();
} }
} }

8
backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookActionHandler.cs

@ -90,7 +90,7 @@ public sealed class WebhookActionHandler : RuleActionHandler<WebhookAction, Webh
protected override async Task<Result> ExecuteJobAsync(WebhookJob job, protected override async Task<Result> ExecuteJobAsync(WebhookJob job,
CancellationToken ct = default) CancellationToken ct = default)
{ {
var httpClient = httpClientFactory.CreateClient(); var httpClient = httpClientFactory.CreateClient("WebhookAction");
var method = HttpMethod.Post; var method = HttpMethod.Post;
@ -110,7 +110,7 @@ public sealed class WebhookActionHandler : RuleActionHandler<WebhookAction, Webh
break; break;
} }
using var request = new HttpRequestMessage(method, job.RequestUrl); var request = new HttpRequestMessage(method, job.RequestUrl);
if (!string.IsNullOrEmpty(job.RequestBody) && job.Method != WebhookMethod.GET) if (!string.IsNullOrEmpty(job.RequestBody) && job.Method != WebhookMethod.GET)
{ {
@ -119,8 +119,6 @@ public sealed class WebhookActionHandler : RuleActionHandler<WebhookAction, Webh
request.Content = new StringContent(job.RequestBody, Encoding.UTF8, mediaType); request.Content = new StringContent(job.RequestBody, Encoding.UTF8, mediaType);
} }
request.Headers.Add("User-Agent", "Squidex Webhook");
if (job.Headers != null) if (job.Headers != null)
{ {
foreach (var (key, value) in job.Headers) foreach (var (key, value) in job.Headers)
@ -134,8 +132,6 @@ public sealed class WebhookActionHandler : RuleActionHandler<WebhookAction, Webh
request.Headers.Add("X-Signature", job.RequestSignature); request.Headers.Add("X-Signature", job.RequestSignature);
} }
request.Headers.Add("X-Application", "Squidex Webhook");
return await httpClient.OneWayRequestAsync(request, job.RequestBody, ct); return await httpClient.OneWayRequestAsync(request, job.RequestBody, ct);
} }
} }

6
backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookPlugin.cs

@ -15,6 +15,12 @@ public sealed class WebhookPlugin : IPlugin
{ {
public void ConfigureServices(IServiceCollection services, IConfiguration config) public void ConfigureServices(IServiceCollection services, IConfiguration config)
{ {
services.AddHttpClient("WebhookPlugin", options =>
{
options.DefaultRequestHeaders.Add("User-Agent", "Squidex Webhook");
options.DefaultRequestHeaders.Add("X-Application", "Squidex Webhook");
});
services.AddRuleAction<WebhookAction, WebhookActionHandler>(); services.AddRuleAction<WebhookAction, WebhookActionHandler>();
} }
} }

20
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/HttpJintExtension.cs

@ -91,20 +91,16 @@ public sealed class HttpJintExtension : IJintExtension, IScriptDescriptor
throw new JavaScriptException("URL is not valid."); throw new JavaScriptException("URL is not valid.");
} }
using (var httpClient = httpClientFactory.CreateClient()) var httpClient = httpClientFactory.CreateClient("Jint");
{
using (var request = CreateRequest(context, method, uri, body, headers))
{
using (var response = await httpClient.SendAsync(request, ct))
{
response.EnsureSuccessStatusCode();
var responseObject = await ParseResponseasync(context, response, ct); var request = CreateRequest(context, method, uri, body, headers);
var response = await httpClient.SendAsync(request, ct);
scheduler.Run(callback, responseObject); response.EnsureSuccessStatusCode();
}
} var responseObject = await ParseResponseasync(context, response, ct);
}
scheduler.Run(callback, responseObject);
}); });
} }

32
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/AtlasIndexDefinition.cs

@ -105,35 +105,31 @@ public static class AtlasIndexDefinition
return "t.iv"; return "t.iv";
} }
public static async Task<string> CreateIndexAsync(AtlasOptions options, public static async Task<string> CreateIndexAsync(AtlasOptions options, IHttpClientFactory httpClientFactory,
string database, string database,
string collectionName, string collectionName,
CancellationToken ct) CancellationToken ct)
{ {
var (index, name) = Create(database, collectionName); var (index, name) = Create(database, collectionName);
using (var httpClient = new HttpClient(new HttpClientHandler var httpClient = httpClientFactory.CreateClient("Atlas");
{
Credentials = new NetworkCredential(options.PublicKey, options.PrivateKey, "cloud.mongodb.com")
}))
{
var url = $"https://cloud.mongodb.com/api/atlas/v1.0/groups/{options.GroupId}/clusters/{options.ClusterName}/fts/indexes";
var result = await httpClient.PostAsJsonAsync(url, index, ct); var url = $"/api/atlas/v1.0/groups/{options.GroupId}/clusters/{options.ClusterName}/fts/indexes";
if (result.IsSuccessStatusCode) var result = await httpClient.PostAsJsonAsync(url, index, ct);
{
return name; if (result.IsSuccessStatusCode)
} {
return name;
}
var error = await result.Content.ReadFromJsonAsync<ErrorResponse>(cancellationToken: ct); var error = await result.Content.ReadFromJsonAsync<ErrorResponse>(cancellationToken: ct);
if (error?.ErrorCode != "ATLAS_FTS_DUPLICATE_INDEX") if (error?.ErrorCode == "ATLAS_FTS_DUPLICATE_INDEX")
{ {
var message = new ConfigurationError($"Creating index failed with {result.StatusCode}: {error?.Detail}"); var message = new ConfigurationError($"Creating index failed with {result.StatusCode}: {error?.Detail}");
throw new ConfigurationException(message); throw new ConfigurationException(message);
}
} }
return name; return name;

6
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/AtlasTextIndex.cs

@ -26,11 +26,13 @@ public sealed class AtlasTextIndex : MongoTextIndexBase<Dictionary<string, strin
new LuceneQueryAnalyzer(LuceneVersion.LUCENE_48, "*", new LuceneQueryAnalyzer(LuceneVersion.LUCENE_48, "*",
new StandardAnalyzer(LuceneVersion.LUCENE_48, CharArraySet.EMPTY_SET)); new StandardAnalyzer(LuceneVersion.LUCENE_48, CharArraySet.EMPTY_SET));
private readonly AtlasOptions options; private readonly AtlasOptions options;
private readonly IHttpClientFactory httpClientFactory;
private string index; private string index;
public AtlasTextIndex(IMongoDatabase database, IOptions<AtlasOptions> options) public AtlasTextIndex(IMongoDatabase database, IHttpClientFactory httpClientFactory, IOptions<AtlasOptions> options)
: base(database) : base(database)
{ {
this.httpClientFactory = httpClientFactory;
this.options = options.Value; this.options = options.Value;
} }
@ -39,7 +41,7 @@ public sealed class AtlasTextIndex : MongoTextIndexBase<Dictionary<string, strin
{ {
await base.SetupCollectionAsync(collection, ct); await base.SetupCollectionAsync(collection, ct);
index = await AtlasIndexDefinition.CreateIndexAsync(options, index = await AtlasIndexDefinition.CreateIndexAsync(options, httpClientFactory,
Database.DatabaseNamespace.DatabaseName, CollectionName(), ct); Database.DatabaseNamespace.DatabaseName, CollectionName(), ct);
} }

82
backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/TemplatesClient.cs

@ -20,65 +20,62 @@ public sealed class TemplatesClient
public TemplatesClient(IHttpClientFactory httpClientFactory, IOptions<TemplatesOptions> options) public TemplatesClient(IHttpClientFactory httpClientFactory, IOptions<TemplatesOptions> options)
{ {
this.httpClientFactory = httpClientFactory; this.httpClientFactory = httpClientFactory;
this.options = options.Value; this.options = options.Value;
} }
public async Task<string?> GetRepositoryUrl(string name, public async Task<string?> GetRepositoryUrl(string name,
CancellationToken ct = default) CancellationToken ct = default)
{ {
using (var httpClient = httpClientFactory.CreateClient()) var httpClient = httpClientFactory.CreateClient();
var result = new List<Template>();
foreach (var repository in options.Repositories.OrEmpty())
{ {
var result = new List<Template>(); var url = $"{repository.ContentUrl}/README.md";
foreach (var repository in options.Repositories.OrEmpty()) var text = await httpClient.GetStringAsync(url, ct);
{
var url = $"{repository.ContentUrl}/README.md";
var text = await httpClient.GetStringAsync(url, ct); foreach (Match match in Regex.Matches(text).OfType<Match>())
{
var currentName = match.Groups["Name"].Value;
foreach (Match match in Regex.Matches(text).OfType<Match>()) if (currentName == name)
{ {
var currentName = match.Groups["Name"].Value; return $"{repository.GitUrl ?? repository.ContentUrl}?folder={name}";
if (currentName == name)
{
return $"{repository.GitUrl ?? repository.ContentUrl}?folder={name}";
}
} }
} }
return null;
} }
return null;
} }
public async Task<List<Template>> GetTemplatesAsync( public async Task<List<Template>> GetTemplatesAsync(
CancellationToken ct = default) CancellationToken ct = default)
{ {
using (var httpClient = httpClientFactory.CreateClient()) var httpClient = httpClientFactory.CreateClient();
{
var result = new List<Template>();
foreach (var repository in options.Repositories.OrEmpty()) var result = new List<Template>();
{
var url = $"{repository.ContentUrl}/README.md";
var text = await httpClient.GetStringAsync(url, ct); foreach (var repository in options.Repositories.OrEmpty())
{
var url = $"{repository.ContentUrl}/README.md";
foreach (Match match in Regex.Matches(text).OfType<Match>()) var text = await httpClient.GetStringAsync(url, ct);
{
var title = match.Groups["Title"].Value;
result.Add(new Template( foreach (Match match in Regex.Matches(text).OfType<Match>())
match.Groups["Name"].Value, {
title, var title = match.Groups["Title"].Value;
match.Groups["Description"].Value,
title.StartsWith("Starter ", StringComparison.OrdinalIgnoreCase)));
}
}
return result; result.Add(new Template(
match.Groups["Name"].Value,
title,
match.Groups["Description"].Value,
title.StartsWith("Starter ", StringComparison.OrdinalIgnoreCase)));
}
} }
return result;
} }
public async Task<string?> GetDetailAsync(string name, public async Task<string?> GetDetailAsync(string name,
@ -86,18 +83,17 @@ public sealed class TemplatesClient
{ {
Guard.NotNullOrEmpty(name); Guard.NotNullOrEmpty(name);
using (var httpClient = httpClientFactory.CreateClient()) var httpClient = httpClientFactory.CreateClient();
foreach (var repository in options.Repositories.OrEmpty())
{ {
foreach (var repository in options.Repositories.OrEmpty()) var url = $"{repository.ContentUrl}/{name}/README.md";
{
var url = $"{repository.ContentUrl}/{name}/README.md";
var response = await httpClient.GetAsync(url, ct); var response = await httpClient.GetAsync(url, ct);
if (response.IsSuccessStatusCode) if (response.IsSuccessStatusCode)
{ {
return await response.Content.ReadAsStringAsync(ct); return await response.Content.ReadAsStringAsync(ct);
}
} }
} }

19
backend/src/Squidex.Domain.Apps.Entities/Backup/TempFolderBackupArchiveLocation.cs

@ -15,10 +15,12 @@ namespace Squidex.Domain.Apps.Entities.Backup;
public sealed class TempFolderBackupArchiveLocation : IBackupArchiveLocation public sealed class TempFolderBackupArchiveLocation : IBackupArchiveLocation
{ {
private readonly IJsonSerializer serializer; private readonly IJsonSerializer serializer;
private readonly IHttpClientFactory httpClientFactory;
public TempFolderBackupArchiveLocation(IJsonSerializer serializer) public TempFolderBackupArchiveLocation(IJsonSerializer serializer, IHttpClientFactory httpClientFactory)
{ {
this.serializer = serializer; this.serializer = serializer;
this.httpClientFactory = httpClientFactory;
} }
public async Task<IBackupReader> OpenReaderAsync(Uri url, DomainId id, public async Task<IBackupReader> OpenReaderAsync(Uri url, DomainId id,
@ -37,17 +39,14 @@ public sealed class TempFolderBackupArchiveLocation : IBackupArchiveLocation
HttpResponseMessage? response = null; HttpResponseMessage? response = null;
try try
{ {
using (var client = new HttpClient()) var httpClient = httpClientFactory.CreateClient("Backup");
{
client.Timeout = TimeSpan.FromHours(1);
response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, ct); response = await httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, ct);
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
await using (var sourceStream = await response.Content.ReadAsStreamAsync(ct)) await using (var sourceStream = await response.Content.ReadAsStreamAsync(ct))
{ {
await sourceStream.CopyToAsync(stream, ct); await sourceStream.CopyToAsync(stream, ct);
}
} }
} }
catch (HttpRequestException ex) catch (HttpRequestException ex)

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

@ -20,8 +20,8 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="CsvHelper" Version="30.0.1" /> <PackageReference Include="CsvHelper" Version="30.0.1" />
<PackageReference Include="GraphQL" Version="7.2.0" /> <PackageReference Include="GraphQL" Version="7.2.1" />
<PackageReference Include="GraphQL.DataLoader" Version="7.2.0" /> <PackageReference Include="GraphQL.DataLoader" Version="7.2.1" />
<PackageReference Include="Meziantou.Analyzer" Version="1.0.756"> <PackageReference Include="Meziantou.Analyzer" Version="1.0.756">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

35
backend/src/Squidex.Web/GraphQL/GraphQLRunner.cs

@ -1,35 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using GraphQL.Server.Transports.AspNetCore;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Net.Http.Headers;
namespace Squidex.Web.GraphQL;
public sealed class GraphQLRunner
{
private readonly GraphQLHttpMiddleware<DummySchema> middleware;
public GraphQLRunner(IServiceProvider serviceProvider)
{
RequestDelegate next = x => Task.CompletedTask;
var options = new GraphQLHttpMiddlewareOptions
{
DefaultResponseContentType = new MediaTypeHeaderValue("application/json")
};
middleware = ActivatorUtilities.CreateInstance<GraphQLHttpMiddleware<DummySchema>>(serviceProvider, next, options);
}
public Task InvokeAsync(HttpContext context)
{
return middleware.InvokeAsync(context);
}
}

6
backend/src/Squidex.Web/Squidex.Web.csproj

@ -13,9 +13,9 @@
<FrameworkReference Include="Microsoft.AspNetCore.App" /> <FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="GraphQL" Version="7.2.0" /> <PackageReference Include="GraphQL" Version="7.2.1" />
<PackageReference Include="GraphQL.SystemTextJson" Version="7.2.0" /> <PackageReference Include="GraphQL.SystemTextJson" Version="7.2.1" />
<PackageReference Include="GraphQL.Server.Transports.AspNetCore" Version="7.1.1" /> <PackageReference Include="GraphQL.Server.Transports.AspNetCore" Version="7.2.0" />
<PackageReference Include="Meziantou.Analyzer" Version="1.0.756"> <PackageReference Include="Meziantou.Analyzer" Version="1.0.756">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

16
backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsSharedController.cs

@ -5,7 +5,9 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using GraphQL.Server.Transports.AspNetCore;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
using Squidex.Areas.Api.Controllers.Contents.Models; using Squidex.Areas.Api.Controllers.Contents.Models;
using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Contents;
@ -21,17 +23,14 @@ public sealed class ContentsSharedController : ApiController
{ {
private readonly IContentQueryService contentQuery; private readonly IContentQueryService contentQuery;
private readonly IContentWorkflow contentWorkflow; private readonly IContentWorkflow contentWorkflow;
private readonly GraphQLRunner graphQLRunner;
public ContentsSharedController(ICommandBus commandBus, public ContentsSharedController(ICommandBus commandBus,
IContentQueryService contentQuery, IContentQueryService contentQuery,
IContentWorkflow contentWorkflow, IContentWorkflow contentWorkflow)
GraphQLRunner graphQLRunner)
: base(commandBus) : base(commandBus)
{ {
this.contentQuery = contentQuery; this.contentQuery = contentQuery;
this.contentWorkflow = contentWorkflow; this.contentWorkflow = contentWorkflow;
this.graphQLRunner = graphQLRunner;
} }
/// <summary> /// <summary>
@ -47,9 +46,14 @@ public sealed class ContentsSharedController : ApiController
[Route("content/{app}/graphql/batch")] [Route("content/{app}/graphql/batch")]
[ApiPermissionOrAnonymous] [ApiPermissionOrAnonymous]
[ApiCosts(2)] [ApiCosts(2)]
public Task GetGraphQL(string app) public IActionResult GetGraphQL(string app)
{ {
return graphQLRunner.InvokeAsync(HttpContext); var options = new GraphQLHttpMiddlewareOptions
{
DefaultResponseContentType = new MediaTypeHeaderValue("application/json")
};
return new GraphQLExecutionActionResult<DummySchema>(options);
} }
/// <summary> /// <summary>

3
backend/src/Squidex/Areas/Api/Controllers/UI/MyUIOptions.cs

@ -50,6 +50,9 @@ public sealed record MyUIOptions
[JsonPropertyName("onlyAdminsCanCreateTeams")] [JsonPropertyName("onlyAdminsCanCreateTeams")]
public bool OnlyAdminsCanCreateTeams { get; set; } public bool OnlyAdminsCanCreateTeams { get; set; }
[JsonPropertyName("markerProject")]
public string MarkerProject { get; set; }
public sealed class MapOptions public sealed class MapOptions
{ {
[JsonPropertyName("type")] [JsonPropertyName("type")]

1
backend/src/Squidex/Areas/Api/Controllers/UI/UIController.cs

@ -27,7 +27,6 @@ public sealed class UIController : ApiController
: base(commandBus) : base(commandBus)
{ {
this.uiOptions = uiOptions.Value; this.uiOptions = uiOptions.Value;
this.appUISettings = appUISettings; this.appUISettings = appUISettings;
} }

33
backend/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs

@ -166,30 +166,29 @@ public sealed class UsersController : ApiController
return new FileCallbackResult("image/png", callback); return new FileCallbackResult("image/png", callback);
} }
using (var client = httpClientFactory.CreateClient()) var httpClient = httpClientFactory.CreateClient("Users");
{
var url = entity.Claims.PictureNormalizedUrl();
if (!string.IsNullOrWhiteSpace(url)) var url = entity.Claims.PictureNormalizedUrl();
{
var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, HttpContext.RequestAborted);
if (response.IsSuccessStatusCode) if (!string.IsNullOrWhiteSpace(url))
{ {
var contentType = response.Content.Headers.ContentType?.ToString()!; var response = await httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, HttpContext.RequestAborted);
var contentStream = await response.Content.ReadAsStreamAsync(HttpContext.RequestAborted);
var etag = response.Headers.ETag; if (response.IsSuccessStatusCode)
{
var contentType = response.Content.Headers.ContentType?.ToString()!;
var contentStream = await response.Content.ReadAsStreamAsync(HttpContext.RequestAborted);
var result = new FileStreamResult(contentStream, contentType); var etag = response.Headers.ETag;
if (!string.IsNullOrWhiteSpace(etag?.Tag)) var result = new FileStreamResult(contentStream, contentType);
{
result.EntityTag = new EntityTagHeaderValue(etag.Tag, etag.IsWeak);
}
return result; if (!string.IsNullOrWhiteSpace(etag?.Tag))
{
result.EntityTag = new EntityTagHeaderValue(etag.Tag, etag.IsWeak);
} }
return result;
} }
} }
} }

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

@ -17,6 +17,8 @@ public static class IdentityServices
services.Configure<MyIdentityOptions>(config, services.Configure<MyIdentityOptions>(config,
"identity"); "identity");
services.AddHttpClient("USers");
services.AddSingletonAs<DefaultUserResolver>() services.AddSingletonAs<DefaultUserResolver>()
.AsOptional<IUserResolver>(); .AsOptional<IUserResolver>();

5
backend/src/Squidex/Config/Domain/BackupsServices.cs

@ -19,6 +19,11 @@ public static class BackupsServices
{ {
public static void AddSquidexBackups(this IServiceCollection services) public static void AddSquidexBackups(this IServiceCollection services)
{ {
services.AddHttpClient("Backup", options =>
{
options.Timeout = TimeSpan.FromHours(1);
});
services.AddSingletonAs<TempFolderBackupArchiveLocation>() services.AddSingletonAs<TempFolderBackupArchiveLocation>()
.As<IBackupArchiveLocation>(); .As<IBackupArchiveLocation>();

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

@ -30,6 +30,8 @@ public static class ContentsServices
services.Configure<TemplatesOptions>(config, services.Configure<TemplatesOptions>(config,
"templates"); "templates");
services.AddHttpClient("Templates");
services.AddSingletonAs(c => new Lazy<IContentQueryService>(c.GetRequiredService<IContentQueryService>)) services.AddSingletonAs(c => new Lazy<IContentQueryService>(c.GetRequiredService<IContentQueryService>))
.AsSelf(); .AsSelf();

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

@ -50,6 +50,8 @@ public static class InfrastructureServices
services.Configure<DiagnoserOptions>(config, services.Configure<DiagnoserOptions>(config,
"diagnostics"); "diagnostics");
services.AddHttpClient("Jint");
services.AddReplicatedCache(); services.AddReplicatedCache();
services.AddAsyncLocalCache(); services.AddAsyncLocalCache();
services.AddBackgroundCache(); services.AddBackgroundCache();

13
backend/src/Squidex/Config/Domain/StoreServices.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Net;
using System.Text.Json; using System.Text.Json;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Distributed;
@ -172,6 +173,18 @@ public static class StoreServices
{ {
services.Configure<AtlasOptions>(config.GetSection("store:mongoDb:atlas")); services.Configure<AtlasOptions>(config.GetSection("store:mongoDb:atlas"));
services.AddHttpClient("Atlas", options =>
{
options.BaseAddress = new Uri("https://cloud.mongodb.com/");
})
.ConfigureHttpMessageHandlerBuilder(builder =>
{
builder.PrimaryHandler = new HttpClientHandler
{
Credentials = new NetworkCredential(atlasOptions.PublicKey, atlasOptions.PrivateKey, "cloud.mongodb.com")
};
});
services.AddSingletonAs<AtlasTextIndex>() services.AddSingletonAs<AtlasTextIndex>()
.AsOptional<ITextIndex>().As<IDeleter>(); .AsOptional<ITextIndex>().As<IDeleter>();
} }

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

@ -117,8 +117,5 @@ public static class WebServices
services.AddSingletonAs<CachingGraphQLResolver>() services.AddSingletonAs<CachingGraphQLResolver>()
.As<IConfigureExecution>(); .As<IConfigureExecution>();
services.AddSingletonAs<GraphQLRunner>()
.AsSelf();
} }
} }

6
backend/src/Squidex/Squidex.csproj

@ -36,9 +36,9 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="AspNet.Security.OAuth.GitHub" Version="7.0.0" /> <PackageReference Include="AspNet.Security.OAuth.GitHub" Version="7.0.0" />
<PackageReference Include="GraphQL" Version="7.2.0" /> <PackageReference Include="GraphQL" Version="7.2.1" />
<PackageReference Include="GraphQL.MicrosoftDI" Version="7.2.0" /> <PackageReference Include="GraphQL.MicrosoftDI" Version="7.2.1" />
<PackageReference Include="GraphQL.SystemTextJson" Version="7.2.0" /> <PackageReference Include="GraphQL.SystemTextJson" Version="7.2.1" />
<PackageReference Include="Meziantou.Analyzer" Version="1.0.756"> <PackageReference Include="Meziantou.Analyzer" Version="1.0.756">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

11
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryFixture.cs

@ -6,7 +6,7 @@
// ========================================================================== // ==========================================================================
using System.Globalization; using System.Globalization;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.DependencyInjection;
using MongoDB.Bson; using MongoDB.Bson;
using MongoDB.Driver; using MongoDB.Driver;
using NodaTime; using NodaTime;
@ -43,7 +43,14 @@ public sealed class AssetsQueryFixture : IAsyncLifetime
mongoClient = new MongoClient(TestConfig.Configuration["mongodb:configuration"]); mongoClient = new MongoClient(TestConfig.Configuration["mongodb:configuration"]);
mongoDatabase = mongoClient.GetDatabase(TestConfig.Configuration["mongodb:database"]); mongoDatabase = mongoClient.GetDatabase(TestConfig.Configuration["mongodb:database"]);
AssetRepository = new MongoAssetRepository(mongoDatabase, A.Fake<ILogger<MongoAssetRepository>>()); var services =
new ServiceCollection()
.AddSingleton(mongoClient)
.AddSingleton(mongoDatabase)
.AddLogging()
.BuildServiceProvider();
AssetRepository = services.GetRequiredService<MongoAssetRepository>();
} }
public Task DisposeAsync() public Task DisposeAsync()

19
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryFixture.cs

@ -7,6 +7,7 @@
using System.Globalization; using System.Globalization;
using LoremNET; using LoremNET;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using MongoDB.Bson; using MongoDB.Bson;
@ -17,7 +18,9 @@ using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.TestHelpers; using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Domain.Apps.Entities.Contents.DomainObject; using Squidex.Domain.Apps.Entities.Contents.DomainObject;
using Squidex.Domain.Apps.Entities.MongoDb.Assets;
using Squidex.Domain.Apps.Entities.MongoDb.Contents; using Squidex.Domain.Apps.Entities.MongoDb.Contents;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Domain.Apps.Entities.TestHelpers;
@ -76,14 +79,16 @@ public abstract class ContentsQueryFixtureBase : IAsyncLifetime
mongoClient = new MongoClient(TestConfig.Configuration["mongodb:configuration"]); mongoClient = new MongoClient(TestConfig.Configuration["mongodb:configuration"]);
mongoDatabase = mongoClient.GetDatabase(TestConfig.Configuration["mongodb:database"]); mongoDatabase = mongoClient.GetDatabase(TestConfig.Configuration["mongodb:database"]);
var appProvider = CreateAppProvider(); var services =
new ServiceCollection()
.AddSingleton(Options.Create(new ContentOptions { OptimizeForSelfHosting = dedicatedCollections }))
.AddSingleton(CreateAppProvider())
.AddSingleton(mongoClient)
.AddSingleton(mongoDatabase)
.AddLogging()
.BuildServiceProvider();
var options = Options.Create(new ContentOptions ContentRepository = services.GetRequiredService<MongoContentRepository>();
{
OptimizeForSelfHosting = dedicatedCollections
});
ContentRepository = new MongoContentRepository(mongoDatabase, appProvider, options, A.Fake<ILogger<MongoContentRepository>>());
} }
public Task DisposeAsync() public Task DisposeAsync()

22
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/AtlasTextIndexFixture.cs

@ -5,7 +5,9 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Net;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Domain.Apps.Core.TestHelpers; using Squidex.Domain.Apps.Core.TestHelpers;
@ -27,7 +29,25 @@ public sealed class AtlasTextIndexFixture : IAsyncLifetime
var options = TestConfig.Configuration.GetSection("atlas").Get<AtlasOptions>()!; var options = TestConfig.Configuration.GetSection("atlas").Get<AtlasOptions>()!;
Index = new AtlasTextIndex(mongoDatabase, Options.Create(options)); var services =
new ServiceCollection()
.AddSingleton(Options.Create(options))
.AddSingleton(mongoClient)
.AddSingleton(mongoDatabase)
.AddHttpClient("Atlas", options =>
{
options.BaseAddress = new Uri("https://cloud.mongodb.com/");
})
.ConfigureHttpMessageHandlerBuilder(builder =>
{
builder.PrimaryHandler = new HttpClientHandler
{
Credentials = new NetworkCredential(options.PublicKey, options.PrivateKey, "cloud.mongodb.com")
};
}).Services
.BuildServiceProvider();
Index = services.GetRequiredService<AtlasTextIndex>();
} }
public Task InitializeAsync() public Task InitializeAsync()

4
backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj

@ -23,8 +23,8 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="FakeItEasy" Version="7.3.1" /> <PackageReference Include="FakeItEasy" Version="7.3.1" />
<PackageReference Include="FluentAssertions" Version="6.8.0" /> <PackageReference Include="FluentAssertions" Version="6.8.0" />
<PackageReference Include="GraphQL" Version="7.2.0" /> <PackageReference Include="GraphQL" Version="7.2.1" />
<PackageReference Include="GraphQL.SystemTextJson" Version="7.2.0" /> <PackageReference Include="GraphQL.SystemTextJson" Version="7.2.1" />
<PackageReference Include="Lorem.Universal.Net" Version="4.0.80" /> <PackageReference Include="Lorem.Universal.Net" Version="4.0.80" />
<PackageReference Include="Meziantou.Analyzer" Version="1.0.756"> <PackageReference Include="Meziantou.Analyzer" Version="1.0.756">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>

11
frontend/package-lock.json

@ -22,6 +22,7 @@
"@babel/runtime": "^7.19.4", "@babel/runtime": "^7.19.4",
"@egjs/hammerjs": "2.0.17", "@egjs/hammerjs": "2.0.17",
"@graphiql/toolkit": "^0.8.0", "@graphiql/toolkit": "^0.8.0",
"@marker.io/browser": "^0.16.0",
"ace-builds": "1.12.0", "ace-builds": "1.12.0",
"angular-gridster2": "15.0.0", "angular-gridster2": "15.0.0",
"angular-mentions": "1.5.0", "angular-mentions": "1.5.0",
@ -4847,6 +4848,11 @@
"@lezer/common": "^0.16.0" "@lezer/common": "^0.16.0"
} }
}, },
"node_modules/@marker.io/browser": {
"version": "0.16.0",
"resolved": "https://registry.npmjs.org/@marker.io/browser/-/browser-0.16.0.tgz",
"integrity": "sha512-G8bCXoPy42TdvQaZQe9Ak/OmsCSXVLgtQFMBlZJrjmjQ4c4tKbGeeyAjonmrSwvkbBleK+dDCiyb7iHhKg+q7Q=="
},
"node_modules/@mdx-js/mdx": { "node_modules/@mdx-js/mdx": {
"version": "1.6.22", "version": "1.6.22",
"dev": true, "dev": true,
@ -35989,6 +35995,11 @@
"@lezer/common": "^0.16.0" "@lezer/common": "^0.16.0"
} }
}, },
"@marker.io/browser": {
"version": "0.16.0",
"resolved": "https://registry.npmjs.org/@marker.io/browser/-/browser-0.16.0.tgz",
"integrity": "sha512-G8bCXoPy42TdvQaZQe9Ak/OmsCSXVLgtQFMBlZJrjmjQ4c4tKbGeeyAjonmrSwvkbBleK+dDCiyb7iHhKg+q7Q=="
},
"@mdx-js/mdx": { "@mdx-js/mdx": {
"version": "1.6.22", "version": "1.6.22",
"dev": true, "dev": true,

1
frontend/package.json

@ -29,6 +29,7 @@
"@babel/runtime": "^7.19.4", "@babel/runtime": "^7.19.4",
"@egjs/hammerjs": "2.0.17", "@egjs/hammerjs": "2.0.17",
"@graphiql/toolkit": "^0.8.0", "@graphiql/toolkit": "^0.8.0",
"@marker.io/browser": "^0.16.0",
"ace-builds": "1.12.0", "ace-builds": "1.12.0",
"angular-gridster2": "15.0.0", "angular-gridster2": "15.0.0",
"angular-mentions": "1.5.0", "angular-mentions": "1.5.0",

1
frontend/src/app/shell/declarations.ts

@ -11,6 +11,7 @@ export * from './pages/forbidden/forbidden-page.component';
export * from './pages/home/home-page.component'; export * from './pages/home/home-page.component';
export * from './pages/internal/apps-menu.component'; export * from './pages/internal/apps-menu.component';
export * from './pages/internal/internal-area.component'; export * from './pages/internal/internal-area.component';
export * from './pages/internal/feedback-menu.component';
export * from './pages/internal/logo.component'; export * from './pages/internal/logo.component';
export * from './pages/internal/notification-dropdown.component'; export * from './pages/internal/notification-dropdown.component';
export * from './pages/internal/notifications-menu.component'; export * from './pages/internal/notifications-menu.component';

3
frontend/src/app/shell/module.ts

@ -7,7 +7,7 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { SqxFrameworkModule, SqxSharedModule } from '@app/shared'; import { SqxFrameworkModule, SqxSharedModule } from '@app/shared';
import { AppAreaComponent, AppsMenuComponent, ForbiddenPageComponent, HomePageComponent, InternalAreaComponent, LeftMenuComponent, LoginPageComponent, LogoComponent, LogoutPageComponent, NotFoundPageComponent, NotificationDropdownComponent, NotificationsMenuComponent, ProfileMenuComponent, SearchMenuComponent, TeamsAreaComponent } from './declarations'; import { AppAreaComponent, AppsMenuComponent, FeedbackMenuComponent, ForbiddenPageComponent, HomePageComponent, InternalAreaComponent, LeftMenuComponent, LoginPageComponent, LogoComponent, LogoutPageComponent, NotFoundPageComponent, NotificationDropdownComponent, NotificationsMenuComponent, ProfileMenuComponent, SearchMenuComponent, TeamsAreaComponent } from './declarations';
@NgModule({ @NgModule({
imports: [ imports: [
@ -24,6 +24,7 @@ import { AppAreaComponent, AppsMenuComponent, ForbiddenPageComponent, HomePageCo
declarations: [ declarations: [
AppAreaComponent, AppAreaComponent,
AppsMenuComponent, AppsMenuComponent,
FeedbackMenuComponent,
ForbiddenPageComponent, ForbiddenPageComponent,
HomePageComponent, HomePageComponent,
InternalAreaComponent, InternalAreaComponent,

5
frontend/src/app/shell/pages/internal/feedback-menu.component.html

@ -0,0 +1,5 @@
<li class="nav-item nav-icon" *ngIf="markerProject">
<span class="nav-link" (click)="capture()">
<i class="icon-lightbulb_outline"></i>
</span>
</li>

2
frontend/src/app/shell/pages/internal/feedback-menu.component.scss

@ -0,0 +1,2 @@
@import 'mixins';
@import 'vars';

44
frontend/src/app/shell/pages/internal/feedback-menu.component.ts

@ -0,0 +1,44 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
import markerSDK, { MarkerSdk } from '@marker.io/browser';
import { UIOptions } from '@app/shared';
@Component({
selector: 'sqx-feedback-menu',
styleUrls: ['./feedback-menu.component.scss'],
templateUrl: './feedback-menu.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FeedbackMenuComponent implements OnInit, OnDestroy {
private widget?: MarkerSdk;
public markerProject = '';
constructor(uiOptions: UIOptions,
) {
this.markerProject = uiOptions.get('markerProject');
}
public ngOnDestroy() {
this.widget?.unload();
}
public async ngOnInit() {
if (!this.markerProject) {
return;
}
this.widget = await markerSDK.loadWidget({ project: this.markerProject });
this.widget.hide();
}
public capture() {
this.widget?.capture('fullscreen');
}
}

1
frontend/src/app/shell/pages/internal/internal-area.component.html

@ -10,6 +10,7 @@
<div class="navbar-nav align-items-center ms-auto"> <div class="navbar-nav align-items-center ms-auto">
<sqx-search-menu></sqx-search-menu> <sqx-search-menu></sqx-search-menu>
<sqx-asset-uploader></sqx-asset-uploader> <sqx-asset-uploader></sqx-asset-uploader>
<sqx-feedback-menu></sqx-feedback-menu>
<sqx-notifications-menu></sqx-notifications-menu> <sqx-notifications-menu></sqx-notifications-menu>
<sqx-profile-menu></sqx-profile-menu> <sqx-profile-menu></sqx-profile-menu>
</div> </div>

1050
frontend/src/app/theme/icomoon/demo.html

File diff suppressed because it is too large

BIN
frontend/src/app/theme/icomoon/fonts/icomoon.eot

Binary file not shown.

1
frontend/src/app/theme/icomoon/fonts/icomoon.svg

@ -154,6 +154,7 @@
<glyph unicode="&#xe990;" glyph-name="fullscreen" d="M598 724.667h212v-212h-84v128h-128v84zM726 212.667v128h84v-212h-212v84h128zM214 512.667v212h212v-84h-128v-128h-84zM298 340.667v-128h128v-84h-212v212h84z" /> <glyph unicode="&#xe990;" glyph-name="fullscreen" d="M598 724.667h212v-212h-84v128h-128v84zM726 212.667v128h84v-212h-212v84h128zM214 512.667v212h212v-84h-128v-128h-84zM298 340.667v-128h128v-84h-212v212h84z" />
<glyph unicode="&#xe991;" glyph-name="wrap_text" d="M726 468.667q70 0 120-50t50-120-50-120-120-50h-86v-86l-128 128 128 128v-86h96q34 0 60 26t26 60-26 60-60 26h-566v84h556zM854 724.667v-84h-684v84h684zM170 128.667v84h256v-84h-256z" /> <glyph unicode="&#xe991;" glyph-name="wrap_text" d="M726 468.667q70 0 120-50t50-120-50-120-120-50h-86v-86l-128 128 128 128v-86h96q34 0 60 26t26 60-26 60-60 26h-566v84h556zM854 724.667v-84h-684v84h684zM170 128.667v84h256v-84h-256z" />
<glyph unicode="&#xe992;" glyph-name="hourglass_top" d="M256 852.667v-256l170-170-170-172v-254h512v256l-170 170 170 170v256h-512zM682 234.667v-150h-340v150l170 170z" /> <glyph unicode="&#xe992;" glyph-name="hourglass_top" d="M256 852.667v-256l170-170-170-172v-254h512v256l-170 170 170 170v256h-512zM682 234.667v-150h-340v150l170 170z" />
<glyph unicode="&#xe993;" glyph-name="lightbulb_outline" d="M634 380.667q92 64 92 174 0 88-63 151t-151 63-151-63-63-151q0-46 27-96t65-78l36-26v-98h172v98zM512 852.667q124 0 211-87t87-211q0-156-128-244v-98q0-18-12-30t-30-12h-256q-18 0-30 12t-12 30v98q-128 88-128 244 0 124 87 211t211 87zM384 42.667v42h256v-42q0-18-12-30t-30-12h-172q-18 0-30 12t-12 30z" />
<glyph unicode="&#xe9ca;" glyph-name="earth" d="M512 960c-282.77 0-512-229.23-512-512s229.23-512 512-512 512 229.23 512 512-229.23 512-512 512zM512-0.002c-62.958 0-122.872 13.012-177.23 36.452l233.148 262.29c5.206 5.858 8.082 13.422 8.082 21.26v96c0 17.674-14.326 32-32 32-112.99 0-232.204 117.462-233.374 118.626-6 6.002-14.14 9.374-22.626 9.374h-128c-17.672 0-32-14.328-32-32v-192c0-12.122 6.848-23.202 17.69-28.622l110.31-55.156v-187.886c-116.052 80.956-192 215.432-192 367.664 0 68.714 15.49 133.806 43.138 192h116.862c8.488 0 16.626 3.372 22.628 9.372l128 128c6 6.002 9.372 14.14 9.372 22.628v77.412c40.562 12.074 83.518 18.588 128 18.588 70.406 0 137.004-16.26 196.282-45.2-4.144-3.502-8.176-7.164-12.046-11.036-36.266-36.264-56.236-84.478-56.236-135.764s19.97-99.5 56.236-135.764c36.434-36.432 85.218-56.264 135.634-56.26 3.166 0 6.342 0.080 9.518 0.236 13.814-51.802 38.752-186.656-8.404-372.334-0.444-1.744-0.696-3.488-0.842-5.224-81.324-83.080-194.7-134.656-320.142-134.656z" /> <glyph unicode="&#xe9ca;" glyph-name="earth" d="M512 960c-282.77 0-512-229.23-512-512s229.23-512 512-512 512 229.23 512 512-229.23 512-512 512zM512-0.002c-62.958 0-122.872 13.012-177.23 36.452l233.148 262.29c5.206 5.858 8.082 13.422 8.082 21.26v96c0 17.674-14.326 32-32 32-112.99 0-232.204 117.462-233.374 118.626-6 6.002-14.14 9.374-22.626 9.374h-128c-17.672 0-32-14.328-32-32v-192c0-12.122 6.848-23.202 17.69-28.622l110.31-55.156v-187.886c-116.052 80.956-192 215.432-192 367.664 0 68.714 15.49 133.806 43.138 192h116.862c8.488 0 16.626 3.372 22.628 9.372l128 128c6 6.002 9.372 14.14 9.372 22.628v77.412c40.562 12.074 83.518 18.588 128 18.588 70.406 0 137.004-16.26 196.282-45.2-4.144-3.502-8.176-7.164-12.046-11.036-36.266-36.264-56.236-84.478-56.236-135.764s19.97-99.5 56.236-135.764c36.434-36.432 85.218-56.264 135.634-56.26 3.166 0 6.342 0.080 9.518 0.236 13.814-51.802 38.752-186.656-8.404-372.334-0.444-1.744-0.696-3.488-0.842-5.224-81.324-83.080-194.7-134.656-320.142-134.656z" />
<glyph unicode="&#xf00a;" glyph-name="grid" d="M292.571 237.714v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM292.571 530.286v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM658.286 237.714v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM292.571 822.857v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM658.286 530.286v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM1024 237.714v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM658.286 822.857v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM1024 530.286v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM1024 822.857v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857z" /> <glyph unicode="&#xf00a;" glyph-name="grid" d="M292.571 237.714v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM292.571 530.286v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM658.286 237.714v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM292.571 822.857v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM658.286 530.286v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM1024 237.714v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM658.286 822.857v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM1024 530.286v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM1024 822.857v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857z" />
<glyph unicode="&#xf0c9;" glyph-name="list1" horiz-adv-x="878" d="M877.714 182.857v-73.143c0-20-16.571-36.571-36.571-36.571h-804.571c-20 0-36.571 16.571-36.571 36.571v73.143c0 20 16.571 36.571 36.571 36.571h804.571c20 0 36.571-16.571 36.571-36.571zM877.714 475.428v-73.143c0-20-16.571-36.571-36.571-36.571h-804.571c-20 0-36.571 16.571-36.571 36.571v73.143c0 20 16.571 36.571 36.571 36.571h804.571c20 0 36.571-16.571 36.571-36.571zM877.714 768v-73.143c0-20-16.571-36.571-36.571-36.571h-804.571c-20 0-36.571 16.571-36.571 36.571v73.143c0 20 16.571 36.571 36.571 36.571h804.571c20 0 36.571-16.571 36.571-36.571z" /> <glyph unicode="&#xf0c9;" glyph-name="list1" horiz-adv-x="878" d="M877.714 182.857v-73.143c0-20-16.571-36.571-36.571-36.571h-804.571c-20 0-36.571 16.571-36.571 36.571v73.143c0 20 16.571 36.571 36.571 36.571h804.571c20 0 36.571-16.571 36.571-36.571zM877.714 475.428v-73.143c0-20-16.571-36.571-36.571-36.571h-804.571c-20 0-36.571 16.571-36.571 36.571v73.143c0 20 16.571 36.571 36.571 36.571h804.571c20 0 36.571-16.571 36.571-36.571zM877.714 768v-73.143c0-20-16.571-36.571-36.571-36.571h-804.571c-20 0-36.571 16.571-36.571 36.571v73.143c0 20 16.571 36.571 36.571 36.571h804.571c20 0 36.571-16.571 36.571-36.571z" />

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 120 KiB

BIN
frontend/src/app/theme/icomoon/fonts/icomoon.ttf

Binary file not shown.

BIN
frontend/src/app/theme/icomoon/fonts/icomoon.woff

Binary file not shown.

2
frontend/src/app/theme/icomoon/selection.json

File diff suppressed because one or more lines are too long

13
frontend/src/app/theme/icomoon/style.css

@ -1,10 +1,10 @@
@font-face { @font-face {
font-family: 'icomoon'; font-family: 'icomoon';
src: url('fonts/icomoon.eot?krjhn9'); src: url('fonts/icomoon.eot?et23le');
src: url('fonts/icomoon.eot?krjhn9#iefix') format('embedded-opentype'), src: url('fonts/icomoon.eot?et23le#iefix') format('embedded-opentype'),
url('fonts/icomoon.ttf?krjhn9') format('truetype'), url('fonts/icomoon.ttf?et23le') format('truetype'),
url('fonts/icomoon.woff?krjhn9') format('woff'), url('fonts/icomoon.woff?et23le') format('woff'),
url('fonts/icomoon.svg?krjhn9#icomoon') format('svg'); url('fonts/icomoon.svg?et23le#icomoon') format('svg');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
font-display: block; font-display: block;
@ -25,6 +25,9 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.icon-lightbulb_outline:before {
content: "\e993";
}
.icon-hourglass_top:before { .icon-hourglass_top:before {
content: "\e992"; content: "\e992";
} }

Loading…
Cancel
Save