diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/ReferencesFieldBuilder.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/ReferencesFieldBuilder.cs new file mode 100644 index 000000000..cbaac29e8 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/ReferencesFieldBuilder.cs @@ -0,0 +1,28 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Schemas.Commands; + +namespace Squidex.Domain.Apps.Entities.Apps.Templates.Builders +{ + public class ReferencesFieldBuilder : FieldBuilder + { + public ReferencesFieldBuilder(UpsertSchemaField field, UpsertCommand schema) + : base(field, schema) + { + } + + public ReferencesFieldBuilder WithSchemaId(Guid id) + { + Properties().SchemaId = id; + + return this; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/SchemaBuilder.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/SchemaBuilder.cs index f163d357f..cb2b0e886 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/SchemaBuilder.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/SchemaBuilder.cs @@ -106,6 +106,15 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates.Builders return this; } + public SchemaBuilder AddReferences(string name, Action configure) + { + var field = AddField(name); + + configure(new ReferencesFieldBuilder(field, command)); + + return this; + } + public SchemaBuilder AddString(string name, Action configure) { var field = AddField(name); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/StringFieldBuilder.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/StringFieldBuilder.cs index 703d96f46..33aff07db 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/StringFieldBuilder.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/StringFieldBuilder.cs @@ -40,6 +40,13 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates.Builders return this; } + public StringFieldBuilder Unique() + { + Properties().IsUnique = true; + + return this; + } + public StringFieldBuilder Pattern(string pattern, string? message = null) { Properties().Pattern = pattern; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/TagsFieldBuilder.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/TagsFieldBuilder.cs index 67a34d62c..25c189352 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/TagsFieldBuilder.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/TagsFieldBuilder.cs @@ -5,6 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Collections.ObjectModel; +using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Entities.Schemas.Commands; namespace Squidex.Domain.Apps.Entities.Apps.Templates.Builders @@ -15,5 +17,12 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates.Builders : base(field, schema) { } + + public TagsFieldBuilder WithAllowedValues(params string[] values) + { + Properties().AllowedValues = new ReadOnlyCollection(values); + + return this; + } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateIdentityCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateIdentityCommandMiddleware.cs index 6bcbe0628..6b3ec885f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateIdentityCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateIdentityCommandMiddleware.cs @@ -51,7 +51,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates return string.Equals(createApp.Template, TemplateName, StringComparison.OrdinalIgnoreCase); } - private static async Task> CreateAuthenticationSchemeSchemaAsync(Func publish) + private static Task CreateAuthenticationSchemeSchemaAsync(Func publish) { var schema = SchemaBuilder.Create("Authentication Schemes") @@ -71,9 +71,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates .Hints("Additional scopes you want from the provider.")) .Build(); - await publish(schema); - - return NamedId.Of(schema.SchemaId, schema.Name); + return publish(schema); } private static Task CreateClientsSchemaAsync(Func publish) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateIdentityV2CommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateIdentityV2CommandMiddleware.cs new file mode 100644 index 000000000..8f8b14631 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateIdentityV2CommandMiddleware.cs @@ -0,0 +1,333 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Fluid; +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>(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 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 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 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 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> CreateApiScopesSchemaAsync(Func 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 publish, NamedId 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 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); + } + } +} diff --git a/backend/src/Squidex/Config/Domain/CommandsServices.cs b/backend/src/Squidex/Config/Domain/CommandsServices.cs index 2ec0f662c..5e8758761 100644 --- a/backend/src/Squidex/Config/Domain/CommandsServices.cs +++ b/backend/src/Squidex/Config/Domain/CommandsServices.cs @@ -107,6 +107,9 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); + services.AddSingletonAs() + .As(); + services.AddSingletonAs() .As(); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Templates/TemplatesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Templates/TemplatesTests.cs index 1e5972f94..6ee7a0e73 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Templates/TemplatesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Templates/TemplatesTests.cs @@ -24,6 +24,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates { new object[] { new CreateBlogCommandMiddleware(), "blog" }, new object[] { new CreateIdentityCommandMiddleware(), "identity" }, + new object[] { new CreateIdentityV2CommandMiddleware(), "identityV2" }, new object[] { new CreateProfileCommandMiddleware(), "profile" } }; diff --git a/frontend/app/features/apps/pages/apps-page.component.html b/frontend/app/features/apps/pages/apps-page.component.html index 491995974..916181a2c 100644 --- a/frontend/app/features/apps/pages/apps-page.component.html +++ b/frontend/app/features/apps/pages/apps-page.component.html @@ -76,39 +76,56 @@ -
+
- +
-

New Profile Sample

+

New Identity App

-
Create your profile page.
+
Create app for Squidex Identity.
- Sample Code at Github + Project
-
+
-

New Identity App

+

New Identity App V2

-
Create app for Squidex Identity.
+
Create app for Squidex Identity V2.
+ +
+
+
+ +
+ +

New Profile Sample

+ +
+
Create your profile page.
+
+ Sample Code at Github +
+
+
+