Browse Source

Cleanup/middlewares (#713)

* Another ng test

* Cleanup

* Formatting.

* Simplified templates.
pull/715/head
Sebastian Stehle 5 years ago
committed by GitHub
parent
commit
c3415d28b8
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 38
      backend/extensions/Squidex.Extensions/Samples/Middleware/TemplateInstance.cs
  2. 54
      backend/extensions/Squidex.Extensions/Samples/Middleware/TemplateMiddleware.cs
  3. 25
      backend/extensions/Squidex.Extensions/Samples/Middleware/TemplatePlugin.cs
  4. 4
      backend/i18n/frontend_en.json
  5. 4
      backend/i18n/frontend_it.json
  6. 4
      backend/i18n/frontend_nl.json
  7. 4
      backend/i18n/source/frontend_en.json
  8. 4
      backend/i18n/source/frontend_it.json
  9. 4
      backend/i18n/source/frontend_nl.json
  10. 2
      backend/i18n/translator/Squidex.Translator/Processes/Helper.cs
  11. 10
      backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/AlwaysCreateClientCommandMiddleware.cs
  12. 10
      backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/ArrayFieldBuilder.cs
  13. 18
      backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/FieldBuilder.cs
  14. 41
      backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateBlog.cs
  15. 281
      backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateIdentityCommandMiddleware.cs
  16. 332
      backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateIdentityV2CommandMiddleware.cs
  17. 47
      backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateProfile.cs
  18. 19
      backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/DefaultScripts.cs
  19. 18
      backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/ITemplate.cs
  20. 53
      backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/TemplateCommandMiddleware.cs
  21. 1
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentGraphType.cs
  22. 12
      backend/src/Squidex/Config/Domain/CommandsServices.cs
  23. 18
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Templates/TemplatesTests.cs
  24. 34
      frontend/app/features/apps/pages/apps-page.component.html

38
backend/extensions/Squidex.Extensions/Samples/Middleware/TemplateInstance.cs

@ -0,0 +1,38 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading.Tasks;
using Squidex.Domain.Apps.Entities.Apps.Templates;
using Squidex.Domain.Apps.Entities.Apps.Templates.Builders;
namespace Squidex.Extensions.Samples.Middleware
{
public class TemplateInstance : ITemplate
{
public string Name { get; } = "custom2";
public Task RunAsync(PublishTemplate publish)
{
var schema =
SchemaBuilder.Create("Blogs")
.AddString("Title", f => f
.Length(100)
.Required())
.AddString("Slug", f => f
.Length(100)
.Required()
.Disabled())
.AddString("Text", f => f
.Length(1000)
.Required()
.AsRichText())
.Build();
return publish(schema);
}
}
}

54
backend/extensions/Squidex.Extensions/Samples/Middleware/TemplateMiddleware.cs

@ -0,0 +1,54 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Domain.Apps.Entities.Apps.Templates.Builders;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
namespace Squidex.Extensions.Samples.Middleware
{
public sealed class TemplateMiddleware : ICustomCommandMiddleware
{
public async Task HandleAsync(CommandContext context, NextDelegate next)
{
await next(context);
if (context.Command is CreateApp createApp && context.IsCompleted && createApp.Template == "custom")
{
var appId = NamedId.Of(createApp.AppId, createApp.Name);
var publish = new Func<IAppCommand, Task>(command =>
{
command.AppId = appId;
return context.CommandBus.PublishAsync(command);
});
var schema =
SchemaBuilder.Create("Pages")
.AddString("Title", f => f
.Length(100)
.Required())
.AddString("Slug", f => f
.Length(100)
.Required()
.Disabled())
.AddString("Text", f => f
.Length(1000)
.Required()
.AsRichText())
.Build();
await publish(schema);
}
}
}
}

25
backend/extensions/Squidex.Extensions/Samples/Middleware/TemplatePlugin.cs

@ -0,0 +1,25 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Domain.Apps.Entities.Apps.Templates;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Plugins;
namespace Squidex.Extensions.Samples.Middleware
{
public sealed class TemplatePlugin : IPlugin
{
public void ConfigureServices(IServiceCollection services, IConfiguration config)
{
services.AddSingleton<ICustomCommandMiddleware, TemplateMiddleware>();
services.AddSingleton<ITemplate, TemplateInstance>();
}
}
}

4
backend/i18n/frontend_en.json

@ -23,10 +23,6 @@
"apps.createBlogApp": "New Blog Sample",
"apps.createBlogAppDescription": "Start with our ready to use blog.",
"apps.createFailed": "Failed to create app. Please reload.",
"apps.createIdentityApp": "New Identity App",
"apps.createIdentityAppDescription": "Create app for Squidex Identity.",
"apps.createIdentityAppV2": "New Identity App V2",
"apps.createIdentityAppV2Description": "Create app for Squidex Identity V2.",
"apps.createProfileApp": "New Profile Sample",
"apps.createProfileAppDescription": "Create your profile page.",
"apps.createWithTemplate": "Create {template} Sample",

4
backend/i18n/frontend_it.json

@ -23,10 +23,6 @@
"apps.createBlogApp": "Nuovo blog",
"apps.createBlogAppDescription": "Inizia con un blog.",
"apps.createFailed": "Non è stato possibile creare l'app. Per favore ricarica.",
"apps.createIdentityApp": "Nuova Identity App",
"apps.createIdentityAppDescription": "Crea un app per Squidex Identity.",
"apps.createIdentityAppV2": "Nuova Identity App V2",
"apps.createIdentityAppV2Description": "Creare un app per Squidex Identity V2.",
"apps.createProfileApp": "Nuovo Profilo",
"apps.createProfileAppDescription": "Crea la tua pagina del profilo.",
"apps.createWithTemplate": "Create un esempio di {template}",

4
backend/i18n/frontend_nl.json

@ -23,10 +23,6 @@
"apps.createBlogApp": "Nieuw blogvoorbeeld",
"apps.createBlogAppDescription": "Begin met onze gebruiksklare blog.",
"apps.createFailed": "Maken van app is mislukt. Laad opnieuw.",
"apps.createIdentityApp": "Nieuwe identiteits-app",
"apps.createIdentityAppDescription": "Maak een app voor Squidex Identity.",
"apps.createIdentityAppV2": "Nieuwe identiteits-app V2",
"apps.createIdentityAppV2Description": "Maak een app voor Squidex Identity V2.",
"apps.createProfileApp": "Nieuw profielvoorbeeld",
"apps.createProfileAppDescription": "Maak uw profielpagina.",
"apps.createWithTemplate": "Maak {sjabloon} voorbeeld",

4
backend/i18n/source/frontend_en.json

@ -23,10 +23,6 @@
"apps.createBlogApp": "New Blog Sample",
"apps.createBlogAppDescription": "Start with our ready to use blog.",
"apps.createFailed": "Failed to create app. Please reload.",
"apps.createIdentityApp": "New Identity App",
"apps.createIdentityAppDescription": "Create app for Squidex Identity.",
"apps.createIdentityAppV2": "New Identity App V2",
"apps.createIdentityAppV2Description": "Create app for Squidex Identity V2.",
"apps.createProfileApp": "New Profile Sample",
"apps.createProfileAppDescription": "Create your profile page.",
"apps.createWithTemplate": "Create {template} Sample",

4
backend/i18n/source/frontend_it.json

@ -23,10 +23,6 @@
"apps.createBlogApp": "Nuovo blog",
"apps.createBlogAppDescription": "Inizia con un blog.",
"apps.createFailed": "Non è stato possibile creare l'app. Per favore ricarica.",
"apps.createIdentityApp": "Nuova Identity App",
"apps.createIdentityAppDescription": "Crea un app per Squidex Identity.",
"apps.createIdentityAppV2": "Nuova Identity App V2",
"apps.createIdentityAppV2Description": "Creare un app per Squidex Identity V2.",
"apps.createProfileApp": "Nuovo Profilo",
"apps.createProfileAppDescription": "Crea la tua pagina del profilo.",
"apps.createWithTemplate": "Create un esempio di {template}",

4
backend/i18n/source/frontend_nl.json

@ -23,10 +23,6 @@
"apps.createBlogApp": "Nieuw blogvoorbeeld",
"apps.createBlogAppDescription": "Begin met onze gebruiksklare blog.",
"apps.createFailed": "Maken van app is mislukt. Laad opnieuw.",
"apps.createIdentityApp": "Nieuwe identiteits-app",
"apps.createIdentityAppDescription": "Maak een app voor Squidex Identity.",
"apps.createIdentityAppV2": "Nieuwe identiteits-app V2",
"apps.createIdentityAppV2Description": "Maak een app voor Squidex Identity V2.",
"apps.createProfileApp": "Nieuw profielvoorbeeld",
"apps.createProfileAppDescription": "Maak uw profielpagina.",
"apps.createWithTemplate": "Maak {sjabloon} voorbeeld",

2
backend/i18n/translator/Squidex.Translator/Processes/Helper.cs

@ -27,7 +27,7 @@ namespace Squidex.Translator.Processes
foreach (var (locale, texts) in service.Translations.Where(x => x.Key != service.MainLocale))
{
Console.WriteLine();
Console.WriteLine("Checking {0}", locale);
Console.WriteLine("----- CHECKING <{0}> -----", locale);
var notTranslated = mainTranslations.Keys.Except(texts.Keys).ToList();
var notRequired = texts.Keys.Except(mainTranslations.Keys).ToList();

10
backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/AlwaysCreateClientCommandMiddleware.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure;
@ -22,9 +23,14 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates
{
var appId = NamedId.Of(createApp.AppId, createApp.Name);
var command = new AttachClient { Id = "default", AppId = appId };
var publish = new Func<IAppCommand, Task>(command =>
{
command.AppId = appId;
return context.CommandBus.PublishAsync(command);
});
await context.CommandBus.PublishAsync(command);
await publish(new AttachClient { Id = "default" });
}
}
}

10
backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/ArrayFieldBuilder.cs

@ -15,10 +15,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates.Builders
{
public class ArrayFieldBuilder : FieldBuilder<ArrayFieldBuilder>
{
protected new UpsertSchemaField field
private UpsertSchemaField TypedField
{
get => base.Field as UpsertSchemaField;
init => base.Field = value;
get => (UpsertSchemaField)Field;
}
public ArrayFieldBuilder(UpsertSchemaField field, CreateSchema schema)
@ -100,11 +99,10 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates.Builders
}
};
field.Nested ??= Array.Empty<UpsertSchemaNestedField>();
field.Nested = field.Nested.Union(new[] { nestedField }).ToArray();
TypedField.Nested ??= Array.Empty<UpsertSchemaNestedField>();
TypedField.Nested = TypedField.Nested.Union(new[] { nestedField }).ToArray();
return nestedField;
}
}
}

18
backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/FieldBuilder.cs

@ -31,14 +31,14 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates.Builders
{
Field.Properties = Field.Properties with { Label = label };
return this as T;
return (T)(object)this;
}
public T Hints(string? hints)
{
Field.Properties = Field.Properties with { Hints = hints };
return this as T;
return (T)(object)this;
}
public T Localizable()
@ -48,26 +48,26 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates.Builders
localizableField.Partitioning = Partitioning.Language.Key;
}
return this as T;
return (T)(object)this;
}
public T Disabled()
{
Field.IsDisabled = true;
return this as T;
return (T)(object)this;
}
public T Required()
{
Field.Properties = Field.Properties with { IsRequired = true };
return this as T;
return (T)(object)this;
}
protected void Properties<T>(Func<T, T> updater) where T : FieldProperties
protected void Properties<TProperties>(Func<TProperties, TProperties> updater) where TProperties : FieldProperties
{
Field.Properties = updater((T)Field.Properties);
Field.Properties = updater((TProperties)Field.Properties);
}
public T ShowInList()
@ -75,7 +75,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates.Builders
Schema.FieldsInLists ??= new FieldNames();
Schema.FieldsInLists.Add(Field.Name);
return this as T;
return (T)(object)this;
}
public T ShowInReferences()
@ -83,7 +83,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates.Builders
Schema.FieldsInReferences ??= new FieldNames();
Schema.FieldsInReferences.Add(Field.Name);
return this as T;
return (T)(object)this;
}
}
}

41
backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateBlogCommandMiddleware.cs → backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateBlog.cs

@ -5,51 +5,26 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Domain.Apps.Entities.Apps.Templates.Builders;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Apps.Templates
{
public sealed class CreateBlogCommandMiddleware : ICommandMiddleware
public sealed class CreateBlog : ITemplate
{
private const string TemplateName = "Blog";
public string Name { get; } = "blog";
public async Task HandleAsync(CommandContext context, NextDelegate next)
public Task RunAsync(PublishTemplate publish)
{
if (context.IsCompleted && context.Command is CreateApp createApp && IsRightTemplate(createApp))
{
var appId = NamedId.Of(createApp.AppId, createApp.Name);
var publish = new Func<ICommand, Task>(command =>
{
if (command is IAppCommand appCommand)
{
appCommand.AppId = appId;
}
return context.CommandBus.PublishAsync(command);
});
await Task.WhenAll(
return Task.WhenAll(
CreatePagesAsync(publish),
CreatePostsAsync(publish));
}
await next(context);
}
private static bool IsRightTemplate(CreateApp createApp)
{
return string.Equals(createApp.Template, TemplateName, StringComparison.OrdinalIgnoreCase);
}
private static async Task CreatePostsAsync(Func<ICommand, Task> publish)
private static async Task CreatePostsAsync(PublishTemplate publish)
{
var postsId = await CreatePostsSchemaAsync(publish);
@ -68,7 +43,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates
});
}
private static async Task CreatePagesAsync(Func<ICommand, Task> publish)
private static async Task CreatePagesAsync(PublishTemplate publish)
{
var pagesId = await CreatePagesSchemaAsync(publish);
@ -87,7 +62,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates
});
}
private static async Task<NamedId<DomainId>> CreatePostsSchemaAsync(Func<ICommand, Task> publish)
private static async Task<NamedId<DomainId>> CreatePostsSchemaAsync(PublishTemplate publish)
{
var schema =
SchemaBuilder.Create("Posts")
@ -112,7 +87,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates
return NamedId.Of(schema.SchemaId, schema.Name);
}
private static async Task<NamedId<DomainId>> CreatePagesSchemaAsync(Func<ICommand, Task> publish)
private static async Task<NamedId<DomainId>> CreatePagesSchemaAsync(PublishTemplate publish)
{
var schema =
SchemaBuilder.Create("Pages")

281
backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateIdentityCommandMiddleware.cs

@ -1,281 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Domain.Apps.Entities.Apps.Templates.Builders;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Apps.Templates
{
public sealed class CreateIdentityCommandMiddleware : ICommandMiddleware
{
private const string TemplateName = "Identity";
public async Task HandleAsync(CommandContext context, NextDelegate next)
{
if (context.IsCompleted && context.Command is CreateApp createApp && IsRightTemplate(createApp))
{
var appId = NamedId.Of(createApp.AppId, createApp.Name);
var publish = new Func<ICommand, Task>(command =>
{
if (command is IAppCommand appCommand)
{
appCommand.AppId = appId;
}
return context.CommandBus.PublishAsync(command);
});
await Task.WhenAll(
CreateApiResourcesSchemaAsync(publish),
CreateAuthenticationSchemeSchemaAsync(publish),
CreateClientsSchemaAsync(publish),
CreateIdentityResourcesSchemaAsync(publish),
CreateSettingsSchemaAsync(publish),
CreateUsersSchemaAsync(publish));
}
await next(context);
}
private static bool IsRightTemplate(CreateApp createApp)
{
return string.Equals(createApp.Template, TemplateName, StringComparison.OrdinalIgnoreCase);
}
private static async Task<NamedId<DomainId>> CreateAuthenticationSchemeSchemaAsync(Func<ICommand, Task> publish)
{
var schema =
SchemaBuilder.Create("Authentication Schemes")
.AddString("Provider", f => f
.AsDropDown("Facebook", "Google", "Microsoft", "Twitter")
.Required()
.ShowInList()
.Hints("The name and type of the provider."))
.AddString("Client Id", f => f
.Required()
.ShowInList()
.Hints("The client id that you must configure at the external provider."))
.AddString("Client Secret", f => f
.Required()
.Hints("The client secret that you must configure at the external provider."))
.AddTags("Scopes", f => f
.Hints("Additional scopes you want from the provider."))
.Build();
await publish(schema);
return NamedId.Of(schema.SchemaId, schema.Name);
}
private static Task CreateClientsSchemaAsync(Func<ICommand, Task> publish)
{
var schema =
SchemaBuilder.Create("Clients")
.AddString("Client Id", f => f
.Required()
.Hints("Unique id of the client."))
.AddString("Client Name", f => f
.Localizable()
.Hints("Client display name (used for logging and consent screen)."))
.AddString("Client Uri", f => f
.Localizable()
.Hints("URI to further information about client (used on consent screen)."))
.AddAssets("Logo", f => f
.MustBeImage()
.Hints("URI to client logo (used on consent screen)."))
.AddTags("Client Secrets", f => f
.Hints("Client secrets - only relevant for flows that require a secret."))
.AddTags("Allowed Scopes", f => f
.Hints("Specifies the api scopes that the client is allowed to request."))
.AddTags("Allowed Grant Types", f => f
.Hints("Specifies the allowed grant types (legal combinations of AuthorizationCode, Implicit, Hybrid, ResourceOwner, ClientCredentials)."))
.AddTags("Redirect Uris", f => f
.Hints("Specifies allowed URIs to return tokens or authorization codes to"))
.AddTags("Post Logout Redirect Uris", f => f
.Hints("Specifies allowed URIs to redirect to after logout."))
.AddTags("Allowed Cors Origins", f => f
.Hints("Gets or sets the allowed CORS origins for JavaScript clients."))
.AddBoolean("Require Consent", f => f
.AsToggle()
.Hints("Specifies whether a consent screen is required."))
.AddBoolean("Allow Offline Access", f => f
.AsToggle()
.Hints("Gets or sets a value indicating whether to allow offline access."))
.Build();
return publish(schema);
}
private static Task CreateSettingsSchemaAsync(Func<ICommand, Task> publish)
{
var schema =
SchemaBuilder.Create("Settings").Singleton()
.AddString("Site Name", f => f
.Localizable()
.Hints("The name of your website."))
.AddAssets("Logo", f => f
.MustBeImage()
.Hints("Logo that is rendered in the header."))
.AddString("Footer Text", f => f
.Localizable()
.Hints("The optional footer text."))
.AddString("PrivacyPolicyUrl", f => f
.Localizable()
.Hints("The link to your privacy policies."))
.AddString("LegalUrl", f => f
.Localizable()
.Hints("The link to your legal information."))
.AddString("Email Confirmation Text", f => f
.AsTextArea()
.Localizable()
.Hints("The text for the confirmation email."))
.AddString("Email Confirmation Subject", f => f
.AsTextArea()
.Localizable()
.Hints("The subject for the confirmation email."))
.AddString("Email Password Reset Text", f => f
.AsTextArea()
.Localizable()
.Hints("The text for the password reset email."))
.AddString("Email Password Reset Subject", f => f
.AsTextArea()
.Localizable()
.Hints("The subject for the password reset email."))
.AddString("Terms of Service Url", f => f
.Localizable()
.Hints("The link to your tems of service."))
.AddString("Bootstrap Url", f => f
.Hints("The link to a custom bootstrap theme."))
.AddString("Styles Url", f => f
.Hints("The link to a stylesheet."))
.AddString("SMTP From", f => f
.Hints("The SMTP sender address."))
.AddString("SMTP Server", f => f
.Hints("The smpt server."))
.AddString("SMTP Username", f => f
.Hints("The username for your SMTP server."))
.AddString("SMTP Password", f => f
.Hints("The password for your SMTP server."))
.AddString("Google Analytics Id", f => f
.Hints("The id to your google analytics account."))
.Build();
return publish(schema);
}
private static async Task CreateUsersSchemaAsync(Func<ICommand, Task> publish)
{
var schema =
SchemaBuilder.Create("Users")
.AddString("Username", f => f
.Required()
.ShowInList()
.Hints("The unique username to login."))
.AddString("Email", f => f
.Pattern(@"^[a-zA-Z0-9.!#$%&’*+\\/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:.[a-zA-Z0-9-]+)*$", "Must be an email address.")
.Required()
.ShowInList()
.Hints("The unique email to login."))
.AddString("Phone Number", f => f
.Hints("Phone number of the user."))
.AddTags("Roles", f => f
.Hints("The roles of the user."))
.AddJson("Claims", f => f
.Hints("The claims of the user."))
.AddBoolean("Email Confirmed", f => f
.AsToggle()
.Hints("Indicates if the email is confirmed."))
.AddBoolean("Phone Number Confirmed", f => f
.AsToggle()
.Hints("Indicates if the phone number is confirmed."))
.AddBoolean("LockoutEnabled", f => f
.AsToggle()
.Hints("Toggle on to lock out the user."))
.AddDateTime("Lockout End Date Utc", f => f
.AsDateTime()
.Disabled()
.Hints("Indicates when the lockout ends."))
.AddTags("Login Keys", f => f
.Disabled()
.Hints("Login information for querying."))
.AddJson("Logins", f => f
.Disabled()
.Hints("Login information."))
.AddJson("Tokens", f => f
.Disabled()
.Hints("Login tokens."))
.AddNumber("Access Failed Count", f => f
.Disabled()
.Hints("The number of failed login attempts."))
.AddString("Password Hash", f => f
.Disabled()
.Hints("The hashed password."))
.AddString("Normalized Email", f => f
.Disabled()
.Hints("The normalized email for querying."))
.AddString("Normalized Username", f => f
.Disabled()
.Hints("The normalized user name for querying."))
.AddString("Security Stamp", f => f
.Disabled()
.Hints("Internal security stamp"))
.WithScripts(DefaultScripts.GenerateUsername)
.Build();
await publish(schema);
}
private static Task CreateApiResourcesSchemaAsync(Func<ICommand, Task> publish)
{
var schema =
SchemaBuilder.Create("API Resources")
.AddString("Name", f => f
.Required()
.ShowInList()
.Hints("The unique name of the API."))
.AddString("Display Name", f => f
.Localizable()
.Hints("The display name of the API."))
.AddString("Description", f => f
.Localizable()
.Hints("The description name of the API."))
.AddTags("User Claims", f => f
.Hints("List of accociated user claims that should be included when this resource is requested."))
.Build();
return publish(schema);
}
private static Task CreateIdentityResourcesSchemaAsync(Func<ICommand, Task> publish)
{
var schema =
SchemaBuilder.Create("Identity Resources")
.AddString("Name", f => f
.Required()
.ShowInList()
.Hints("The unique name of the identity information."))
.AddString("Display Name", f => f
.Localizable()
.Hints("The display name of the identity information."))
.AddString("Description", f => f
.Localizable()
.Hints("The description name of the identity information."))
.AddTags("User Claims", f => f
.Hints("List of accociated user claims that should be included when this resource is requested."))
.AddBoolean("Required", f => f
.Hints("Specifies whether the user can de-select the scope on the consent screen."))
.Build();
return publish(schema);
}
}
}

332
backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateIdentityV2CommandMiddleware.cs

@ -1,332 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Domain.Apps.Entities.Apps.Templates.Builders;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Apps.Templates
{
public sealed class CreateIdentityV2CommandMiddleware : ICommandMiddleware
{
private const string TemplateName = "IdentityV2";
public async Task HandleAsync(CommandContext context, NextDelegate next)
{
if (context.IsCompleted && context.Command is CreateApp createApp && IsRightTemplate(createApp))
{
var appId = NamedId.Of(createApp.AppId, createApp.Name);
var publish = new Func<ICommand, Task<CommandContext>>(command =>
{
if (command is IAppCommand appCommand)
{
appCommand.AppId = appId;
}
return context.CommandBus.PublishAsync(command);
});
var apiScopeId = await CreateApiScopesSchemaAsync(publish);
await Task.WhenAll(
CreateApiResourcesSchemaAsync(publish, apiScopeId),
CreateAuthenticationSchemeSchemaAsync(publish),
CreateClientsSchemaAsync(publish),
CreateIdentityResourcesSchemaAsync(publish),
CreateSettingsSchemaAsync(publish),
CreateUsersSchemaAsync(publish));
}
await next(context);
}
private static bool IsRightTemplate(CreateApp createApp)
{
return string.Equals(createApp.Template, TemplateName, StringComparison.OrdinalIgnoreCase);
}
private static Task CreateAuthenticationSchemeSchemaAsync(Func<ICommand, Task> publish)
{
var schema =
SchemaBuilder.Create("Authentication Schemes")
.AddString("Provider", f => f
.AsDropDown("Facebook", "Google", "Microsoft", "Twitter")
.Unique()
.Required()
.ShowInList()
.Hints("The name and type of the provider."))
.AddString("Client Id", f => f
.Required()
.ShowInList()
.Hints("The client id that you must configure at the external provider."))
.AddString("Client Secret", f => f
.Required()
.Hints("The client secret that you must configure at the external provider."))
.AddTags("Scopes", f => f
.Hints("Additional scopes you want from the provider."))
.Build();
return publish(schema);
}
private static Task CreateClientsSchemaAsync(Func<ICommand, Task> publish)
{
var schema =
SchemaBuilder.Create("Clients")
.AddString("Client Id", f => f
.Unique()
.Required()
.Hints("Unique id of the client."))
.AddString("Client Name", f => f
.Localizable()
.Hints("Client display name (used for logging and consent screen)."))
.AddString("Client Uri", f => f
.Localizable()
.Hints("URI to further information about client (used on consent screen)."))
.AddAssets("Logo", f => f
.MustBeImage()
.Hints("URI to client logo (used on consent screen)."))
.AddBoolean("Require Consent", f => f
.AsToggle()
.Hints("Specifies whether a consent screen is required."))
.AddBoolean("Disabled", f => f
.AsToggle()
.Hints("Enable or disable the client."))
.AddBoolean("Allow Offline Access", f => f
.AsToggle()
.Hints("Gets or sets a value indicating whether to allow offline access."))
.AddTags("Allowed Grant Types", f => f
.WithAllowedValues("implicit", "hybrid", "authorization_code", "client_credentials")
.Hints("Specifies the allowed grant types."))
.AddTags("Client Secrets", f => f
.Hints("Client secrets - only relevant for flows that require a secret."))
.AddTags("Allowed Scopes", f => f
.Hints("Specifies the api scopes that the client is allowed to request."))
.AddTags("Redirect Uris", f => f
.Hints("Specifies allowed URIs to return tokens or authorization codes to"))
.AddTags("Post Logout Redirect Uris", f => f
.Hints("Specifies allowed URIs to redirect to after logout."))
.AddTags("Allowed Cors Origins", f => f
.Hints("Gets or sets the allowed CORS origins for JavaScript clients."))
.Build();
return publish(schema);
}
private static Task CreateSettingsSchemaAsync(Func<ICommand, Task> publish)
{
var schema =
SchemaBuilder.Create("Settings").Singleton()
.AddString("Site Name", f => f
.Localizable()
.Hints("The name of your website."))
.AddAssets("Logo", f => f
.MustBeImage()
.Hints("Logo that is rendered in the header."))
.AddString("Footer Text", f => f
.Localizable()
.Hints("The optional footer text."))
.AddString("PrivacyPolicyUrl", f => f
.Localizable()
.Hints("The link to your privacy policies."))
.AddString("LegalUrl", f => f
.Localizable()
.Hints("The link to your legal information."))
.AddString("Email Confirmation Text", f => f
.AsTextArea()
.Localizable()
.Hints("The text for the confirmation email."))
.AddString("Email Confirmation Subject", f => f
.AsTextArea()
.Localizable()
.Hints("The subject for the confirmation email."))
.AddString("Email Password Reset Text", f => f
.AsTextArea()
.Localizable()
.Hints("The text for the password reset email."))
.AddString("Email Password Reset Subject", f => f
.AsTextArea()
.Localizable()
.Hints("The subject for the password reset email."))
.AddString("Terms of Service Url", f => f
.Localizable()
.Hints("The link to your tems of service."))
.AddString("Bootstrap Url", f => f
.Hints("The link to a custom bootstrap theme."))
.AddString("Styles Url", f => f
.Hints("The link to a stylesheet."))
.AddString("SMTP From", f => f
.Hints("The SMTP sender address."))
.AddString("SMTP Server", f => f
.Hints("The smpt server."))
.AddString("SMTP Username", f => f
.Hints("The username for your SMTP server."))
.AddString("SMTP Password", f => f
.Hints("The password for your SMTP server."))
.AddString("Google Analytics Id", f => f
.Hints("The id to your google analytics account."))
.Build();
return publish(schema);
}
private static async Task CreateUsersSchemaAsync(Func<ICommand, Task> publish)
{
var schema =
SchemaBuilder.Create("Users")
.AddString("Username", f => f
.Required()
.ShowInList()
.Hints("The unique username to login."))
.AddString("Email", f => f
.Pattern(@"^[a-zA-Z0-9.!#$%&’*+\\/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:.[a-zA-Z0-9-]+)*$", "Must be an email address.")
.Required()
.ShowInList()
.Hints("The unique email to login."))
.AddString("Phone Number", f => f
.Hints("Phone number of the user."))
.AddTags("Roles", f => f
.Hints("The roles of the user."))
.AddJson("Claims", f => f
.Hints("The claims of the user."))
.AddBoolean("Email Confirmed", f => f
.AsToggle()
.Hints("Indicates if the email is confirmed."))
.AddBoolean("Phone Number Confirmed", f => f
.AsToggle()
.Hints("Indicates if the phone number is confirmed."))
.AddBoolean("LockoutEnabled", f => f
.AsToggle()
.Hints("Toggle on to lock out the user."))
.AddDateTime("Lockout End Date Utc", f => f
.AsDateTime()
.Disabled()
.Hints("Indicates when the lockout ends."))
.AddTags("Login Keys", f => f
.Disabled()
.Hints("Login information for querying."))
.AddJson("Logins", f => f
.Disabled()
.Hints("Login information."))
.AddJson("Tokens", f => f
.Disabled()
.Hints("Login tokens."))
.AddNumber("Access Failed Count", f => f
.Disabled()
.Hints("The number of failed login attempts."))
.AddString("Password Hash", f => f
.Disabled()
.Hints("The hashed password."))
.AddString("Normalized Email", f => f
.Disabled()
.Hints("The normalized email for querying."))
.AddString("Normalized Username", f => f
.Disabled()
.Hints("The normalized user name for querying."))
.AddString("Security Stamp", f => f
.Disabled()
.Hints("Internal security stamp"))
.WithScripts(DefaultScripts.GenerateUsername)
.Build();
await publish(schema);
}
private static async Task<NamedId<DomainId>> CreateApiScopesSchemaAsync(Func<ICommand, Task> publish)
{
var schema =
SchemaBuilder.Create("API Scopes")
.AddString("Name", f => f
.Unique()
.Required()
.ShowInList()
.Hints("The unique name of the API scope."))
.AddString("Display Name", f => f
.Localizable()
.Hints("The display name of the API scope."))
.AddString("Description", f => f
.Localizable()
.Hints("The description name of the API scope."))
.AddBoolean("Disabled", f => f
.AsToggle()
.Hints("Enable or disable the scope."))
.AddBoolean("Emphasize", f => f
.AsToggle()
.Hints("Emphasize the API scope for important scopes."))
.AddTags("User Claims", f => f
.Hints("List of accociated user claims that should be included when this resource is requested."))
.Build();
await publish(schema);
return NamedId.Of(schema.SchemaId, schema.Name);
}
private static Task CreateApiResourcesSchemaAsync(Func<ICommand, Task> publish, NamedId<DomainId> scopeId)
{
var schema =
SchemaBuilder.Create("API Resources")
.AddString("Name", f => f
.Unique()
.Required()
.ShowInList()
.Hints("The unique name of the API."))
.AddString("Display Name", f => f
.Localizable()
.Hints("The display name of the API."))
.AddString("Description", f => f
.Localizable()
.Hints("The description name of the API."))
.AddBoolean("Disabled", f => f
.AsToggle()
.Hints("Enable or disable the API."))
.AddReferences("Scopes", f => f
.WithSchemaId(scopeId.Id)
.Hints("The scopes for this API."))
.AddTags("User Claims", f => f
.Hints("List of accociated user claims that should be included when this resource is requested."))
.Build();
return publish(schema);
}
private static Task CreateIdentityResourcesSchemaAsync(Func<ICommand, Task> publish)
{
var schema =
SchemaBuilder.Create("Identity Resources")
.AddString("Name", f => f
.Unique()
.Required()
.ShowInList()
.Hints("The unique name of the identity information."))
.AddString("Display Name", f => f
.Localizable()
.Hints("The display name of the identity information."))
.AddString("Description", f => f
.Localizable()
.Hints("The description name of the identity information."))
.AddBoolean("Required", f => f
.AsToggle()
.Hints("Specifies whether the user can de-select the scope on the consent screen."))
.AddBoolean("Disabled", f => f
.AsToggle()
.Hints("Enable or disable the scope."))
.AddBoolean("Emphasize", f => f
.AsToggle()
.Hints("Emphasize the API scope for important scopes."))
.AddTags("User Claims", f => f
.Hints("List of accociated user claims that should be included when this resource is requested."))
.Build();
return publish(schema);
}
}
}

47
backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateProfileCommandMiddleware.cs → backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateProfile.cs

@ -5,38 +5,21 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Domain.Apps.Entities.Apps.Templates.Builders;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Apps.Templates
{
public sealed class CreateProfileCommandMiddleware : ICommandMiddleware
public sealed class CreateProfile : ITemplate
{
private const string TemplateName = "Profile";
public string Name { get; } = "profile";
public async Task HandleAsync(CommandContext context, NextDelegate next)
public Task RunAsync(PublishTemplate publish)
{
if (context.IsCompleted && context.Command is CreateApp createApp && IsRightTemplate(createApp))
{
var appId = NamedId.Of(createApp.AppId, createApp.Name);
var publish = new Func<ICommand, Task>(command =>
{
if (command is IAppCommand appCommand)
{
appCommand.AppId = appId;
}
return context.CommandBus.PublishAsync(command);
});
await Task.WhenAll(
return Task.WhenAll(
CreateBasicsAsync(publish),
CreateEducationSchemaAsync(publish),
CreateExperienceSchemaAsync(publish),
@ -45,15 +28,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates
CreateSkillsSchemaAsync(publish));
}
await next(context);
}
private static bool IsRightTemplate(CreateApp createApp)
{
return string.Equals(createApp.Template, TemplateName, StringComparison.OrdinalIgnoreCase);
}
private static async Task CreateBasicsAsync(Func<ICommand, Task> publish)
private static async Task CreateBasicsAsync(PublishTemplate publish)
{
var postsId = await CreateBasicsSchemaAsync(publish);
@ -75,7 +50,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates
});
}
private static async Task<NamedId<DomainId>> CreateBasicsSchemaAsync(Func<ICommand, Task> publish)
private static async Task<NamedId<DomainId>> CreateBasicsSchemaAsync(PublishTemplate publish)
{
var command =
SchemaBuilder.Create("Basics")
@ -118,7 +93,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates
return NamedId.Of(command.SchemaId, command.Name);
}
private static async Task<NamedId<DomainId>> CreateProjectsSchemaAsync(Func<ICommand, Task> publish)
private static async Task<NamedId<DomainId>> CreateProjectsSchemaAsync(PublishTemplate publish)
{
var schema =
SchemaBuilder.Create("Projects")
@ -148,7 +123,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates
return NamedId.Of(schema.SchemaId, schema.Name);
}
private static async Task<NamedId<DomainId>> CreateExperienceSchemaAsync(Func<ICommand, Task> publish)
private static async Task<NamedId<DomainId>> CreateExperienceSchemaAsync(PublishTemplate publish)
{
var schema =
SchemaBuilder.Create("Experience")
@ -175,7 +150,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates
return NamedId.Of(schema.SchemaId, schema.Name);
}
private static async Task<NamedId<DomainId>> CreateEducationSchemaAsync(Func<ICommand, Task> publish)
private static async Task<NamedId<DomainId>> CreateEducationSchemaAsync(PublishTemplate publish)
{
var schema =
SchemaBuilder.Create("Education")
@ -202,7 +177,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates
return NamedId.Of(schema.SchemaId, schema.Name);
}
private static async Task<NamedId<DomainId>> CreatePublicationsSchemaAsync(Func<ICommand, Task> publish)
private static async Task<NamedId<DomainId>> CreatePublicationsSchemaAsync(PublishTemplate publish)
{
var command =
SchemaBuilder.Create("Publications")
@ -224,7 +199,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates
return NamedId.Of(command.SchemaId, command.Name);
}
private static async Task<NamedId<DomainId>> CreateSkillsSchemaAsync(Func<ICommand, Task> publish)
private static async Task<NamedId<DomainId>> CreateSkillsSchemaAsync(PublishTemplate publish)
{
var command =
SchemaBuilder.Create("Skills")

19
backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/DefaultScripts.cs

@ -20,29 +20,10 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates
replace(data);
}";
private const string ScriptToGenerateUsername = @"
var data = ctx.data;
if (data.userName && data.userName.iv) {
data.normalizedUserName = { iv: data.userName.iv.toUpperCase() };
}
if (data.email && data.email.iv) {
data.normalizedEmail = { iv: data.email.iv.toUpperCase() };
}
replace(data);";
public static readonly SchemaScripts GenerateSlug = new SchemaScripts
{
Create = ScriptToGenerateSlug,
Update = ScriptToGenerateSlug
};
public static readonly SchemaScripts GenerateUsername = new SchemaScripts
{
Create = ScriptToGenerateUsername,
Update = ScriptToGenerateUsername
};
}
}

18
backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/ITemplate.cs

@ -0,0 +1,18 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading.Tasks;
namespace Squidex.Domain.Apps.Entities.Apps.Templates
{
public interface ITemplate
{
string Name { get; }
Task RunAsync(PublishTemplate publish);
}
}

53
backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/TemplateCommandMiddleware.cs

@ -0,0 +1,53 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Apps.Templates
{
public delegate Task PublishTemplate(IAppCommand command);
public sealed class TemplateCommandMiddleware : ICommandMiddleware
{
private readonly Dictionary<string, ITemplate> templates;
public TemplateCommandMiddleware(IEnumerable<ITemplate> templates)
{
Guard.NotNull(templates, nameof(templates));
this.templates = templates.ToDictionary(x => x.Name, StringComparer.OrdinalIgnoreCase);
}
public async Task HandleAsync(CommandContext context, NextDelegate next)
{
if (context.IsCompleted && context.Command is CreateApp createApp && !string.IsNullOrWhiteSpace(createApp.Template))
{
if (templates.TryGetValue(createApp.Template, out var template))
{
var appId = NamedId.Of(createApp.AppId, createApp.Name);
var publish = new PublishTemplate(command =>
{
command.AppId = appId;
return context.CommandBus.PublishAsync(command);
});
await template.RunAsync(publish);
}
}
await next(context);
}
}
}

1
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentGraphType.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using GraphQL.Types;

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

@ -1,4 +1,4 @@
// ==========================================================================
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
@ -103,14 +103,14 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<AlwaysCreateClientCommandMiddleware>()
.As<ICommandMiddleware>();
services.AddSingletonAs<CreateBlogCommandMiddleware>()
services.AddSingletonAs<TemplateCommandMiddleware>()
.As<ICommandMiddleware>();
services.AddSingletonAs<CreateIdentityCommandMiddleware>()
.As<ICommandMiddleware>();
services.AddSingletonAs<CreateBlog>()
.As<ITemplate>();
services.AddSingletonAs<CreateProfileCommandMiddleware>()
.As<ICommandMiddleware>();
services.AddSingletonAs<CreateProfile>()
.As<ITemplate>();
services.AddSingletonAs<UsageTrackerCommandMiddleware>()
.As<ICommandMiddleware>();

18
backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Templates/TemplatesTests.cs

@ -6,6 +6,7 @@
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FakeItEasy;
using Squidex.Domain.Apps.Entities.Apps.Commands;
@ -22,24 +23,27 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates
public static readonly IEnumerable<object[]> TemplateTests = new[]
{
new object[] { new CreateBlogCommandMiddleware(), "blog" },
new object[] { new CreateIdentityCommandMiddleware(), "identity" },
new object[] { new CreateProfileCommandMiddleware(), "profile" }
new object[] { new CreateBlog() },
new object[] { new CreateProfile() }
};
[Theory]
[MemberData(nameof(TemplateTests))]
public async Task Should_create_schemas(ICommandMiddleware middleware, string template)
public async Task Should_create_schemas(ITemplate template)
{
var command = new CreateApp { AppId = DomainId.NewGuid(), Name = "my-app", Template = template };
var appId = NamedId.Of(DomainId.NewGuid(), "my-app");
var command = new CreateApp { AppId = appId.Id, Name = appId.Name, Template = template.Name };
var context =
new CommandContext(command, commandBus)
.Complete();
await middleware.HandleAsync(context);
var sut = new TemplateCommandMiddleware(Enumerable.Repeat(template, 1));
await sut.HandleAsync(context);
A.CallTo(() => commandBus.PublishAsync(A<CreateSchema>._))
A.CallTo(() => commandBus.PublishAsync(A<CreateSchema>.That.Matches(x => x.AppId == appId)))
.MustHaveHappened();
}
}

34
frontend/app/features/apps/pages/apps-page.component.html

@ -53,40 +53,6 @@
</div>
</div>
<div class="card card-template card-href" (click)="createNewApp('Identity')">
<div class="card-body">
<div class="card-image">
<img src="./images/add-identity.svg">
</div>
<h3 class="card-title">{{ 'apps.createIdentityApp' | sqxTranslate }}</h3>
<div class="card-text">
<div>{{ 'apps.createIdentityAppDescription' | sqxTranslate }}</div>
<div>
<a href="https://github.com/Squidex/squidex-identity" sqxStopClick sqxExternalLink>{{ 'common.project' | sqxTranslate }}</a>
</div>
</div>
</div>
</div>
<div class="card card-template card-href" (click)="createNewApp('IdentityV2')">
<div class="card-body">
<div class="card-image">
<img src="./images/add-identity.svg">
</div>
<h3 class="card-title">{{ 'apps.createIdentityAppV2' | sqxTranslate }}</h3>
<div class="card-text">
<div>{{ 'apps.createIdentityAppV2Description' | sqxTranslate }}</div>
<div>
<a href="https://github.com/Squidex/squidex-identity" sqxStopClick sqxExternalLink>{{ 'common.project' | sqxTranslate }}</a>
</div>
</div>
</div>
</div>
<div class="card card-template card-href" (click)="createNewApp('Profile')">
<div class="card-body">
<div class="card-image">

Loading…
Cancel
Save