diff --git a/backend/i18n/frontend_en.json b/backend/i18n/frontend_en.json index b3f7b0219..bea65cdbf 100644 --- a/backend/i18n/frontend_en.json +++ b/backend/i18n/frontend_en.json @@ -851,7 +851,7 @@ "start.login": "Login to Squidex", "start.loginHint": "The login button will open a new popup. Once you are logged in successful we will redirect you to the Squidex management portal.", "start.madeBy": "Proudly made by", - "start.madeByCopyright": "Sebastian Stehle and Contributors, 2016-2020", + "start.madeByCopyright": "Sebastian Stehle and Contributors, 2016-2021", "tour.joinForum": "Join our Forum", "tour.joinGithub": "Join us on Github", "tour.skip": "Skip Tour", diff --git a/backend/i18n/source/backend_en.json b/backend/i18n/source/backend_en.json index 44c66589e..ac1ece3ff 100644 --- a/backend/i18n/source/backend_en.json +++ b/backend/i18n/source/backend_en.json @@ -45,6 +45,7 @@ "common.clientSecret": "Client Secret", "common.contentType": "Content type", "common.contributorId": "Contributor ID or email", + "common.critical": "Critial", "common.data": "Data", "common.defaultValue": "Default value", "common.displayName": "Display name", @@ -112,8 +113,10 @@ "common.save": "Save", "common.schemaId": "Schema ID", "common.signup": "Signup", + "common.success": "Success", "common.text": "Text", "common.trigger": "Trigger", + "common.warning": "Warning", "common.workflow": "Workflow", "common.workflowStep": "Step", "common.workflowTransition": "Transition", @@ -269,6 +272,28 @@ "search.contentsResult": "{name} Contents", "search.schemaResult": "{name} Schema", "security.passwordStolen": "This password has previously appeared in a data breach and should never be used. If you have ever used it anywhere before, change it!", + "setup.createUser.button": "Create User", + "setup.createUser.confirmPassword": "Confirm", + "setup.createUser.failure": "Neither password authentication nor an external authentication provider such as Google is configured. Please check your settings and logs.", + "setup.createUser.headline": "Admin User", + "setup.createUser.headlineCreate": "Create Admin User", + "setup.createUser.loginHint": "You have configured at least one external authentication provider such as Google. Just go to the login page and login to become administrator.", + "setup.createUser.loginLink": "Go to Login Page.", + "setup.createUser.separator": "OR", + "setup.headline": "Installation", + "setup.hint": "You are seeing this screen because no user exists yet. After a user is created, you are not able to use this screen again.", + "setup.madeBy": "Proudly made by", + "setup.madeByCopyright": "Sebastian Stehle and Contributors, 2016-2021", + "setup.ruleAppCreation.warningAdmins": "With your setup, only admins can create new apps. If you want to change this set UI__ONLYADMINSCANCREATEAPPS=false as environment variable.", + "setup.ruleAppCreation.warningAll": "With your setup, every user can create new apps. If you want to change this set UI__ONLYADMINSCANCREATEAPPS=true as environment variable.", + "setup.ruleFolder.warning": "You are using the folder asset store where all assets are stored in the file system. Please remember to include the asset folder into your backup strategy and map it to a volume, if you are using Docker.", + "setup.ruleFtp.warning": "You are using the FTP asset store. It is not recommended to use this storage type because of bad performance.", + "setup.ruleHttps.failure": " You are not accessing the site over https. If this warning is not correct then Squidex cannot detect https mode, because your instance is behind a reverse proxy such as nginx. Ensure that http headers are forwarded properly, via the X-Forwarded-* headers.", + "setup.ruleHttps.success": "Congratulations, you are accessing your Squidex installation over a secure connection (https).", + "setup.rules.headline": "System Checklist", + "setup.ruleUrl.failure": "You should access Squidex only over one one canonical URL and configure this URL over the URLS__BASEURL environment variable. The current base URL {actual} does not match to the base url {configured}.", + "setup.ruleUrl.success": "Congratulations the URLS__BASEURL environment variable is configured properly.", + "setup.title": "Installation", "users.accessDenied.text": "This operation is not allowed, your account might be locked.", "users.accessDenied.title": "Access denied", "users.consent.agree": "I agree!", diff --git a/backend/i18n/source/frontend_en.json b/backend/i18n/source/frontend_en.json index b3f7b0219..bea65cdbf 100644 --- a/backend/i18n/source/frontend_en.json +++ b/backend/i18n/source/frontend_en.json @@ -851,7 +851,7 @@ "start.login": "Login to Squidex", "start.loginHint": "The login button will open a new popup. Once you are logged in successful we will redirect you to the Squidex management portal.", "start.madeBy": "Proudly made by", - "start.madeByCopyright": "Sebastian Stehle and Contributors, 2016-2020", + "start.madeByCopyright": "Sebastian Stehle and Contributors, 2016-2021", "tour.joinForum": "Join our Forum", "tour.joinGithub": "Join us on Github", "tour.skip": "Skip Tour", diff --git a/backend/src/Squidex.Domain.Users/DefaultUserService.cs b/backend/src/Squidex.Domain.Users/DefaultUserService.cs index 6ddfa98be..b08d311cf 100644 --- a/backend/src/Squidex.Domain.Users/DefaultUserService.cs +++ b/backend/src/Squidex.Domain.Users/DefaultUserService.cs @@ -44,7 +44,7 @@ namespace Squidex.Domain.Users public async Task IsEmptyAsync() { - var result = await QueryAsync(null, 0, 0); + var result = await QueryAsync(null, 1, 0); return result.Total == 0; } diff --git a/backend/src/Squidex.Shared/Texts.it.resx b/backend/src/Squidex.Shared/Texts.it.resx index f51d4b34e..056aa9eb2 100644 --- a/backend/src/Squidex.Shared/Texts.it.resx +++ b/backend/src/Squidex.Shared/Texts.it.resx @@ -220,6 +220,9 @@ ID o Email del collaboratore + + Critial + Data @@ -421,12 +424,18 @@ Iscriviti + + Success + Testo Trigger + + Warning + Workflow @@ -892,6 +901,72 @@ Questa password risulta essere stata compromessa e non dovrebbe essere mai utilizzata. Se l'hai utilizzata in precedenza, cambiala! + + Create User + + + Confirm + + + Neither password authentication nor an external authentication provider such as Google is configured. Please check your settings and logs. + + + Admin User + + + Create Admin User + + + You have configured at least one external authentication provider such as Google. Just go to the login page and login to become administrator. + + + Go to Login Page. + + + OR + + + Installation + + + You are seeing this screen because no user exists yet. After a user is created, you are not able to use this screen again. + + + Proudly made by + + + Sebastian Stehle and Contributors, 2016-2021 + + + With your setup, only admins can create new apps. If you want to change this set <code>UI__ONLYADMINCANCREATEAPPS=false</code> as environment variable. + + + With your setup, every user can create new apps. If you want to change this set <code>UI__ONLYADMINCANCREATEAPPS=true</code> as environment variable. + + + You are using the <strong>folder asset store</strong> where all assets are stored in the file system. Please remember to include the asset folder into your backup strategy and map it to a volume, if you are using Docker. + + + You are using the <strong>FTP asset store</strong>. It is not recommended to use this storage type because of bad performance. + + + You are not accessing the site over https. If this warning is not correct then Squidex cannot detect https mode, because your instance is behind a reverse proxy such as nginx. Ensure that http headers are forwarded properly, via the <code>X-Forwarded-*</code> headers. + + + Congratulations, you are accessing your Squidex installation over a secure connection (https). + + + System Checklist + + + You should access Squidex only over one one canonical URL and configure this URL over the <code>URLS__BASEURL</code> environment variable. The current base URL <code>{actual}</code> does not match to the base url <code>{configured}</code>. + + + Congratulations the <code>URLS__BASEURL</code> environment variable is configured properly. + + + Installation + Questa operazione non è consentita, il tuo account potrebbe essere bloccato. diff --git a/backend/src/Squidex.Shared/Texts.nl.resx b/backend/src/Squidex.Shared/Texts.nl.resx index edee5068b..d3fc592a5 100644 --- a/backend/src/Squidex.Shared/Texts.nl.resx +++ b/backend/src/Squidex.Shared/Texts.nl.resx @@ -220,6 +220,9 @@ Bijdrager-ID of e-mailadres + + Critial + Gegevens @@ -421,12 +424,18 @@ Aanmelden + + Success + Tekst Trigger + + Warning + Workflow @@ -892,6 +901,72 @@ Dit wachtwoord is eerder verschenen bij een datalek en mag nooit worden gebruikt. Als je het ooit eerder hebt gebruikt, verander het dan! + + Create User + + + Confirm + + + Neither password authentication nor an external authentication provider such as Google is configured. Please check your settings and logs. + + + Admin User + + + Create Admin User + + + You have configured at least one external authentication provider such as Google. Just go to the login page and login to become administrator. + + + Go to Login Page. + + + OR + + + Installation + + + You are seeing this screen because no user exists yet. After a user is created, you are not able to use this screen again. + + + Proudly made by + + + Sebastian Stehle and Contributors, 2016-2021 + + + With your setup, only admins can create new apps. If you want to change this set <code>UI__ONLYADMINCANCREATEAPPS=false</code> as environment variable. + + + With your setup, every user can create new apps. If you want to change this set <code>UI__ONLYADMINCANCREATEAPPS=true</code> as environment variable. + + + You are using the <strong>folder asset store</strong> where all assets are stored in the file system. Please remember to include the asset folder into your backup strategy and map it to a volume, if you are using Docker. + + + You are using the <strong>FTP asset store</strong>. It is not recommended to use this storage type because of bad performance. + + + You are not accessing the site over https. If this warning is not correct then Squidex cannot detect https mode, because your instance is behind a reverse proxy such as nginx. Ensure that http headers are forwarded properly, via the <code>X-Forwarded-*</code> headers. + + + Congratulations, you are accessing your Squidex installation over a secure connection (https). + + + System Checklist + + + You should access Squidex only over one one canonical URL and configure this URL over the <code>URLS__BASEURL</code> environment variable. The current base URL <code>{actual}</code> does not match to the base url <code>{configured}</code>. + + + Congratulations the <code>URLS__BASEURL</code> environment variable is configured properly. + + + Installation + Deze bewerking is niet toegestaan, je account is mogelijk vergrendeld. diff --git a/backend/src/Squidex.Shared/Texts.resx b/backend/src/Squidex.Shared/Texts.resx index 9507a3795..77332a5be 100644 --- a/backend/src/Squidex.Shared/Texts.resx +++ b/backend/src/Squidex.Shared/Texts.resx @@ -220,6 +220,9 @@ Contributor ID or email + + Critial + Data @@ -421,12 +424,18 @@ Signup + + Success + Text Trigger + + Warning + Workflow @@ -892,6 +901,72 @@ This password has previously appeared in a data breach and should never be used. If you have ever used it anywhere before, change it! + + Create User + + + Confirm + + + Neither password authentication nor an external authentication provider such as Google is configured. Please check your settings and logs. + + + Admin User + + + Create Admin User + + + You have configured at least one external authentication provider such as Google. Just go to the login page and login to become administrator. + + + Go to Login Page. + + + OR + + + Installation + + + You are seeing this screen because no user exists yet. After a user is created, you are not able to use this screen again. + + + Proudly made by + + + Sebastian Stehle and Contributors, 2016-2021 + + + With your setup, only admins can create new apps. If you want to change this set <code>UI__ONLYADMINCANCREATEAPPS=false</code> as environment variable. + + + With your setup, every user can create new apps. If you want to change this set <code>UI__ONLYADMINCANCREATEAPPS=true</code> as environment variable. + + + You are using the <strong>folder asset store</strong> where all assets are stored in the file system. Please remember to include the asset folder into your backup strategy and map it to a volume, if you are using Docker. + + + You are using the <strong>FTP asset store</strong>. It is not recommended to use this storage type because of bad performance. + + + You are not accessing the site over https. If this warning is not correct then Squidex cannot detect https mode, because your instance is behind a reverse proxy such as nginx. Ensure that http headers are forwarded properly, via the <code>X-Forwarded-*</code> headers. + + + Congratulations, you are accessing your Squidex installation over a secure connection (https). + + + System Checklist + + + You should access Squidex only over one one canonical URL and configure this URL over the <code>URLS__BASEURL</code> environment variable. The current base URL <code>{actual}</code> does not match to the base url <code>{configured}</code>. + + + Congratulations the <code>URLS__BASEURL</code> environment variable is configured properly. + + + Installation + This operation is not allowed, your account might be locked. diff --git a/backend/src/Squidex.Web/Pipeline/LocalCacheMiddleware.cs b/backend/src/Squidex.Web/Pipeline/LocalCacheMiddleware.cs index 3a69604aa..01b566b4e 100644 --- a/backend/src/Squidex.Web/Pipeline/LocalCacheMiddleware.cs +++ b/backend/src/Squidex.Web/Pipeline/LocalCacheMiddleware.cs @@ -8,22 +8,19 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Squidex.Caching; -using Squidex.Infrastructure; namespace Squidex.Web.Pipeline { - public sealed class LocalCacheMiddleware : IMiddleware + public sealed class LocalCacheMiddleware { - private readonly ILocalCache localCache; + private readonly RequestDelegate next; - public LocalCacheMiddleware(ILocalCache localCache) + public LocalCacheMiddleware(RequestDelegate next) { - Guard.NotNull(localCache, nameof(localCache)); - - this.localCache = localCache; + this.next = next; } - public async Task InvokeAsync(HttpContext context, RequestDelegate next) + public async Task InvokeAsync(HttpContext context, ILocalCache localCache) { using (localCache.StartContext()) { diff --git a/backend/src/Squidex.Web/Pipeline/RequestExceptionMiddleware.cs b/backend/src/Squidex.Web/Pipeline/RequestExceptionMiddleware.cs index 038b64f2e..7069cbdc8 100644 --- a/backend/src/Squidex.Web/Pipeline/RequestExceptionMiddleware.cs +++ b/backend/src/Squidex.Web/Pipeline/RequestExceptionMiddleware.cs @@ -12,35 +12,28 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Routing; -using Squidex.Infrastructure; using Squidex.Log; namespace Squidex.Web.Pipeline { - public sealed class RequestExceptionMiddleware : IMiddleware + public sealed class RequestExceptionMiddleware { private static readonly ActionDescriptor EmptyActionDescriptor = new ActionDescriptor(); private static readonly RouteData EmptyRouteData = new RouteData(); - private readonly IActionResultExecutor resultWriter; - private readonly ISemanticLog log; + private readonly RequestDelegate next; - public RequestExceptionMiddleware(IActionResultExecutor resultWriter, ISemanticLog log) + public RequestExceptionMiddleware(RequestDelegate next) { - Guard.NotNull(resultWriter, nameof(resultWriter)); - Guard.NotNull(log, nameof(log)); - - this.resultWriter = resultWriter; - - this.log = log; + this.next = next; } - public async Task InvokeAsync(HttpContext context, RequestDelegate next) + public async Task InvokeAsync(HttpContext context, IActionResultExecutor writer, ISemanticLog log) { if (context.Request.Query.TryGetValue("error", out var header) && int.TryParse(header, out var statusCode) && IsErrorStatusCode(statusCode)) { var (error, _) = ApiExceptionConverter.ToErrorDto(statusCode, context); - await WriteErrorAsync(context, error); + await WriteErrorAsync(context, error, writer); return; } @@ -56,7 +49,7 @@ namespace Squidex.Web.Pipeline { var (error, _) = ex.ToErrorDto(context); - await WriteErrorAsync(context, error); + await WriteErrorAsync(context, error, writer); } } @@ -64,16 +57,16 @@ namespace Squidex.Web.Pipeline { var (error, _) = ApiExceptionConverter.ToErrorDto(context.Response.StatusCode, context); - await WriteErrorAsync(context, error); + await WriteErrorAsync(context, error, writer); } } - private async Task WriteErrorAsync(HttpContext context, ErrorDto error) + private static async Task WriteErrorAsync(HttpContext context, ErrorDto error, IActionResultExecutor writer) { var actionRouteData = context.GetRouteData() ?? EmptyRouteData; var actionContext = new ActionContext(context, actionRouteData, EmptyActionDescriptor); - await resultWriter.ExecuteAsync(actionContext, new ObjectResult(error) + await writer.ExecuteAsync(actionContext, new ObjectResult(error) { StatusCode = error.StatusCode }); diff --git a/backend/src/Squidex.Web/Pipeline/RequestLogPerformanceMiddleware.cs b/backend/src/Squidex.Web/Pipeline/RequestLogPerformanceMiddleware.cs index a8e7f6305..9dd4b3270 100644 --- a/backend/src/Squidex.Web/Pipeline/RequestLogPerformanceMiddleware.cs +++ b/backend/src/Squidex.Web/Pipeline/RequestLogPerformanceMiddleware.cs @@ -14,22 +14,19 @@ using Squidex.Log; namespace Squidex.Web.Pipeline { - public sealed class RequestLogPerformanceMiddleware : IMiddleware + public sealed class RequestLogPerformanceMiddleware { private readonly RequestLogOptions requestLogOptions; - private readonly ISemanticLog log; + private readonly RequestDelegate next; - public RequestLogPerformanceMiddleware(IOptions requestLogOptions, ISemanticLog log) + public RequestLogPerformanceMiddleware(RequestDelegate next, IOptions requestLogOptions) { - Guard.NotNull(requestLogOptions, nameof(requestLogOptions)); - Guard.NotNull(log, nameof(log)); - this.requestLogOptions = requestLogOptions.Value; - this.log = log; + this.next = next; } - public async Task InvokeAsync(HttpContext context, RequestDelegate next) + public async Task InvokeAsync(HttpContext context, ISemanticLog log) { var watch = ValueStopwatch.StartNew(); diff --git a/backend/src/Squidex.Web/Pipeline/SetupMiddleware.cs b/backend/src/Squidex.Web/Pipeline/SetupMiddleware.cs new file mode 100644 index 000000000..d6a63cd79 --- /dev/null +++ b/backend/src/Squidex.Web/Pipeline/SetupMiddleware.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 Microsoft.AspNetCore.Http; +using Squidex.Domain.Users; + +namespace Squidex.Web.Pipeline +{ + public sealed class SetupMiddleware + { + private readonly RequestDelegate next; + private bool isUserFound; + + public SetupMiddleware(RequestDelegate next) + { + this.next = next; + } + + public async Task InvokeAsync(HttpContext context, IUserService userService) + { + if (!isUserFound && await userService.IsEmptyAsync()) + { + context.Response.Redirect("/identity-server/setup"); + } + else + { + isUserFound = true; + + await next(context); + } + } + } +} diff --git a/backend/src/Squidex.Web/Pipeline/UsageMiddleware.cs b/backend/src/Squidex.Web/Pipeline/UsageMiddleware.cs index a39bd1a6a..4df691188 100644 --- a/backend/src/Squidex.Web/Pipeline/UsageMiddleware.cs +++ b/backend/src/Squidex.Web/Pipeline/UsageMiddleware.cs @@ -18,18 +18,17 @@ namespace Squidex.Web.Pipeline { public sealed class UsageMiddleware : IMiddleware { - private readonly IAppLogStore logStore; + private readonly IAppLogStore usageLog; private readonly IApiUsageTracker usageTracker; private readonly IClock clock; - public UsageMiddleware(IAppLogStore logStore, IApiUsageTracker usageTracker, IClock clock) + public UsageMiddleware(IAppLogStore usageLog, IApiUsageTracker usageTracker, IClock clock) { - Guard.NotNull(logStore, nameof(logStore)); + Guard.NotNull(usageLog, nameof(usageLog)); Guard.NotNull(usageTracker, nameof(usageTracker)); Guard.NotNull(clock, nameof(clock)); - this.logStore = logStore; - + this.usageLog = usageLog; this.usageTracker = usageTracker; this.clock = clock; @@ -73,7 +72,7 @@ namespace Squidex.Web.Pipeline request.UserClientId = clientId; request.UserId = context.User.OpenIdSubject(); - await logStore.LogAsync(appId.Id, request); + await usageLog.LogAsync(appId.Id, request); if (request.Costs > 0) { diff --git a/backend/src/Squidex/Areas/Frontend/Startup.cs b/backend/src/Squidex/Areas/Frontend/Startup.cs index 6c3f5f74c..f9eed0435 100644 --- a/backend/src/Squidex/Areas/Frontend/Startup.cs +++ b/backend/src/Squidex/Areas/Frontend/Startup.cs @@ -15,6 +15,7 @@ using Microsoft.Extensions.Hosting; using Microsoft.Net.Http.Headers; using Squidex.Areas.Frontend.Middlewares; using Squidex.Pipeline.Squid; +using Squidex.Web.Pipeline; namespace Squidex.Areas.Frontend { @@ -24,9 +25,15 @@ namespace Squidex.Areas.Frontend { var environment = app.ApplicationServices.GetRequiredService(); - app.UseMiddleware(); + app.Map("/squid.svg", builder => builder.UseMiddleware()); + app.UseMiddleware(); + var indexFile = + environment.IsProduction() ? + new PathString("/build/index.html") : + new PathString("/index.html"); + app.Use((context, next) => { if (context.Request.Path == "/client-callback-popup") @@ -39,19 +46,17 @@ namespace Squidex.Areas.Frontend } else if (!Path.HasExtension(context.Request.Path.Value)) { - if (environment.IsDevelopment()) - { - context.Request.Path = new PathString("/index.html"); - } - else - { - context.Request.Path = new PathString("/build/index.html"); - } + context.Request.Path = indexFile; } return next(); }); + app.UseWhen(x => x.Request.Path.StartsWithSegments(indexFile), builder => + { + builder.UseMiddleware(); + }); + app.UseMiddleware(); if (environment.IsDevelopment()) diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs index c47ecdcc4..6d5f1e3f1 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs @@ -31,7 +31,6 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account { public sealed class AccountController : IdentityServerController { - private readonly SignInManager signInManager; private readonly IUserService userService; private readonly IUrlGenerator urlGenerator; private readonly MyIdentityOptions identityOptions; @@ -39,7 +38,6 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account private readonly IIdentityServerInteractionService interactions; public AccountController( - SignInManager signInManager, IUserService userService, IUrlGenerator urlGenerator, IOptions identityOptions, @@ -48,7 +46,6 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account { this.identityOptions = identityOptions.Value; this.interactions = interactions; - this.signInManager = signInManager; this.urlGenerator = urlGenerator; this.userService = userService; this.log = log; @@ -139,7 +136,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account [Route("account/logout/")] public async Task Logout(string logoutId) { - await signInManager.SignOutAsync(); + await SignInManager.SignOutAsync(); if (User.Identity?.IsAuthenticated == true) { @@ -165,7 +162,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account [Route("account/logout-redirect/")] public async Task LogoutRedirect() { - await signInManager.SignOutAsync(); + await SignInManager.SignOutAsync(); return RedirectToAction(nameof(LogoutCompleted)); } @@ -194,7 +191,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account return await LoginViewAsync(returnUrl, true, true); } - var result = await signInManager.PasswordSignInAsync(model.Email, model.Password, true, true); + var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, true, true); if (!result.Succeeded && result.IsLockedOut) { @@ -214,14 +211,14 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account { var allowPasswordAuth = identityOptions.AllowPasswordAuth; - var externalProviders = await signInManager.GetExternalProvidersAsync(); + var externalProviders = await SignInManager.GetExternalProvidersAsync(); if (externalProviders.Count == 1 && !allowPasswordAuth) { var provider = externalProviders[0].AuthenticationScheme; var properties = - signInManager.ConfigureExternalAuthenticationProperties(provider, + SignInManager.ConfigureExternalAuthenticationProperties(provider, Url.Action(nameof(ExternalCallback), new { ReturnUrl = returnUrl })); return Challenge(properties, provider); @@ -230,10 +227,9 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account var vm = new LoginVM { ExternalProviders = externalProviders, - IsLogin = isLogin, IsFailed = isFailed, + IsLogin = isLogin, HasPasswordAuth = allowPasswordAuth, - HasPasswordAndExternal = allowPasswordAuth && externalProviders.Any(), ReturnUrl = returnUrl }; @@ -245,7 +241,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account public IActionResult External(string provider, string? returnUrl = null) { var properties = - signInManager.ConfigureExternalAuthenticationProperties(provider, + SignInManager.ConfigureExternalAuthenticationProperties(provider, Url.Action(nameof(ExternalCallback), new { ReturnUrl = returnUrl })); return Challenge(properties, provider); @@ -255,14 +251,14 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account [Route("account/external-callback/")] public async Task ExternalCallback(string? returnUrl = null) { - var externalLogin = await signInManager.GetExternalLoginInfoWithDisplayNameAsync(); + var externalLogin = await SignInManager.GetExternalLoginInfoWithDisplayNameAsync(); if (externalLogin == null) { return RedirectToAction(nameof(Login)); } - var result = await signInManager.ExternalLoginSignInAsync(externalLogin.LoginProvider, externalLogin.ProviderKey, true); + var result = await SignInManager.ExternalLoginSignInAsync(externalLogin.LoginProvider, externalLogin.ProviderKey, true); if (!result.Succeeded && result.IsLockedOut) { @@ -349,7 +345,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account private async Task<(bool Success, bool Locked)> LoginAsync(UserLoginInfo externalLogin) { - var result = await signInManager.ExternalLoginSignInAsync(externalLogin.LoginProvider, externalLogin.ProviderKey, true); + var result = await SignInManager.ExternalLoginSignInAsync(externalLogin.LoginProvider, externalLogin.ProviderKey, true); return (result.Succeeded, result.IsLockedOut); } @@ -365,17 +361,5 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account return Redirect("~/../"); } } - - private IActionResult RedirectToReturnUrl(string? returnUrl) - { - if (urlGenerator.IsAllowedHost(returnUrl) || interactions.IsValidReturnUrl(returnUrl)) - { - return Redirect(returnUrl); - } - else - { - return Redirect("~/../"); - } - } } } diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/LoginVM.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/LoginVM.cs index 35d8cc8f5..85e331724 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/LoginVM.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/LoginVM.cs @@ -6,6 +6,7 @@ // ========================================================================== using System.Collections.Generic; +using System.Linq; namespace Squidex.Areas.IdentityServer.Controllers.Account { @@ -19,7 +20,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account public bool HasPasswordAuth { get; set; } - public bool HasPasswordAndExternal { get; set; } + public bool HasExternalLogin => ExternalProviders.Any(); public IReadOnlyList ExternalProviders { get; set; } } diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Error/ErrorController.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Error/ErrorController.cs index 528d504d2..1ba465716 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Controllers/Error/ErrorController.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Error/ErrorController.cs @@ -18,20 +18,18 @@ namespace Squidex.Areas.IdentityServer.Controllers.Error public sealed class ErrorController : IdentityServerController { private readonly IIdentityServerInteractionService interaction; - private readonly SignInManager signInManager; - public ErrorController(IIdentityServerInteractionService interaction, SignInManager signInManager) + public ErrorController(IIdentityServerInteractionService interaction) { this.interaction = interaction; - this.signInManager = signInManager; } [Route("error/")] public async Task Error(string? errorId = null) { - await signInManager.SignOutAsync(); + await SignInManager.SignOutAsync(); - var vm = new ErrorViewModel(); + var vm = new ErrorVM(); if (!string.IsNullOrWhiteSpace(errorId)) { diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Error/ErrorViewModel.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Error/ErrorVM.cs similarity index 94% rename from backend/src/Squidex/Areas/IdentityServer/Controllers/Error/ErrorViewModel.cs rename to backend/src/Squidex/Areas/IdentityServer/Controllers/Error/ErrorVM.cs index 2251d3c1f..141070081 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Controllers/Error/ErrorViewModel.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Error/ErrorVM.cs @@ -9,7 +9,7 @@ using IdentityServer4.Models; namespace Squidex.Areas.IdentityServer.Controllers.Error { - public class ErrorViewModel + public class ErrorVM { public ErrorMessage Error { get; set; } } diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/IdentityServerController.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/IdentityServerController.cs index 5f98eaaeb..2449f6d03 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Controllers/IdentityServerController.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/IdentityServerController.cs @@ -6,14 +6,26 @@ // ========================================================================== using System; +using IdentityServer4.Services; +using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; +using Squidex.Hosting; namespace Squidex.Areas.IdentityServer.Controllers { [Area("IdentityServer")] public abstract class IdentityServerController : Controller { + public SignInManager SignInManager + { + get + { + return HttpContext.RequestServices.GetRequiredService>(); + } + } + public override void OnActionExecuting(ActionExecutingContext context) { var request = context.HttpContext.Request; @@ -23,5 +35,29 @@ namespace Squidex.Areas.IdentityServer.Controllers context.Result = new NotFoundResult(); } } + + protected IActionResult RedirectToReturnUrl(string? returnUrl) + { + if (string.IsNullOrWhiteSpace(returnUrl)) + { + return Redirect("~/../"); + } + + var urlGenerator = HttpContext.RequestServices.GetRequiredService(); + + if (urlGenerator.IsAllowedHost(returnUrl)) + { + return Redirect(returnUrl); + } + + var interactions = HttpContext.RequestServices.GetRequiredService(); + + if (interactions.IsValidReturnUrl(returnUrl)) + { + return Redirect(returnUrl); + } + + return Redirect("~/../"); + } } } diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs index 02ec61b7e..030375c2b 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs @@ -33,20 +33,17 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile public sealed class ProfileController : IdentityServerController { private static readonly ResizeOptions ResizeOptions = new ResizeOptions { Width = 128, Height = 128, Mode = ResizeMode.Crop }; - private readonly SignInManager signInManager; private readonly IUserPictureStore userPictureStore; private readonly IUserService userService; private readonly IAssetThumbnailGenerator assetThumbnailGenerator; private readonly MyIdentityOptions identityOptions; public ProfileController( - SignInManager signInManager, + IOptions identityOptions, IUserPictureStore userPictureStore, IUserService userService, - IAssetThumbnailGenerator assetThumbnailGenerator, - IOptions identityOptions) + IAssetThumbnailGenerator assetThumbnailGenerator) { - this.signInManager = signInManager; this.identityOptions = identityOptions.Value; this.userPictureStore = userPictureStore; this.userService = userService; @@ -59,7 +56,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile { var user = await userService.GetAsync(User); - return View(await GetProfileVM(user, successMessage: successMessage)); + return View(await GetVM(user, successMessage: successMessage)); } [HttpPost] @@ -69,7 +66,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); var properties = - signInManager.ConfigureExternalAuthenticationProperties(provider, + SignInManager.ConfigureExternalAuthenticationProperties(provider, Url.Action(nameof(AddLoginCallback)), userService.GetUserId(User)); return Challenge(properties, provider); @@ -148,7 +145,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile private async Task AddLoginAsync(string id) { - var externalLogin = await signInManager.GetExternalLoginInfoWithDisplayNameAsync(id); + var externalLogin = await SignInManager.GetExternalLoginInfoWithDisplayNameAsync(id); await userService.AddLoginAsync(id, externalLogin); } @@ -192,7 +189,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile if (!ModelState.IsValid) { - return View(nameof(Profile), await GetProfileVM(user, model)); + return View(nameof(Profile), await GetVM(user, model)); } string errorMessage; @@ -200,7 +197,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile { await action(user.Id); - await signInManager.SignInAsync((IdentityUser)user.Identity, true); + await SignInManager.SignInAsync((IdentityUser)user.Identity, true); return RedirectToAction(nameof(Profile), new { successMessage }); } @@ -213,10 +210,10 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile errorMessage = T.Get("users.errorHappened"); } - return View(nameof(Profile), await GetProfileVM(user, model, errorMessage)); + return View(nameof(Profile), await GetVM(user, model, errorMessage)); } - private async Task GetProfileVM(IUser? user, TModel? model = null, string? errorMessage = null, string? successMessage = null) where TModel : class + private async Task GetVM(IUser? user, TModel? model = null, string? errorMessage = null, string? successMessage = null) where TModel : class { if (user == null) { @@ -224,11 +221,11 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile } var (providers, hasPassword, logins) = await AsyncHelper.WhenAll( - signInManager.GetExternalProvidersAsync(), + SignInManager.GetExternalProvidersAsync(), userService.HasPasswordAsync(user), userService.GetLoginsAsync(user)); - var result = new ProfileVM + var vm = new ProfileVM { Id = user.Id, ClientSecret = user.Claims.ClientSecret()!, @@ -245,12 +242,12 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile if (model != null) { - SimpleMapper.Map(model, result); + SimpleMapper.Map(model, vm); } - result.Properties ??= user.Claims.GetCustomProperties().Select(UserProperty.FromTuple).ToList(); + vm.Properties ??= user.Claims.GetCustomProperties().Select(UserProperty.FromTuple).ToList(); - return result; + return vm; } } } diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Setup/CreateUserModel.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Setup/CreateUserModel.cs new file mode 100644 index 000000000..db1c3c7fb --- /dev/null +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Setup/CreateUserModel.cs @@ -0,0 +1,23 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure.Validation; + +namespace Squidex.Areas.IdentityServer.Controllers.Setup +{ + public sealed class CreateUserModel + { + [LocalizedRequired] + public string Email { get; set; } + + [LocalizedRequired] + public string Password { get; set; } + + [LocalizedRequiredAttribute] + public string PasswordConfirm { get; set; } + } +} diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Setup/SetupController.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Setup/SetupController.cs new file mode 100644 index 000000000..4e44de4e2 --- /dev/null +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Setup/SetupController.cs @@ -0,0 +1,126 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Squidex.Areas.Api.Controllers.UI; +using Squidex.Assets; +using Squidex.Config; +using Squidex.Domain.Users; +using Squidex.Hosting; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.Translations; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Areas.IdentityServer.Controllers.Setup +{ + public class SetupController : IdentityServerController + { + private readonly IAssetStore assetStore; + private readonly IUrlGenerator urlGenerator; + private readonly IUserService userService; + private readonly MyUIOptions uiOptions; + private readonly MyIdentityOptions identityOptions; + + public SetupController( + IAssetStore assetStore, + IOptions uiOptions, + IOptions identityOptions, + IUrlGenerator urlGenerator, + IUserService userService) + { + this.assetStore = assetStore; + this.identityOptions = identityOptions.Value; + this.uiOptions = uiOptions.Value; + this.urlGenerator = urlGenerator; + this.userService = userService; + } + + [HttpGet] + [Route("setup/")] + public async Task Setup() + { + if (!await userService.IsEmptyAsync()) + { + return RedirectToReturnUrl(null); + } + + return View(nameof(Setup), await GetVM(None.Value)); + } + + [HttpPost] + [Route("setup/")] + public async Task Setup(CreateUserModel model) + { + if (!await userService.IsEmptyAsync()) + { + return RedirectToReturnUrl(null); + } + + if (!ModelState.IsValid) + { + return View(nameof(Profile), await GetVM(model)); + } + + string errorMessage; + try + { + var user = await userService.CreateAsync(model.Email, new UserValues + { + Password = model.Password + }); + + await SignInManager.SignInAsync((IdentityUser)user.Identity, true); + + return RedirectToReturnUrl(null); + } + catch (ValidationException ex) + { + errorMessage = ex.Message; + } + catch (Exception) + { + errorMessage = T.Get("users.errorHappened"); + } + + return View(nameof(Setup), await GetVM(model, errorMessage)); + } + + private async Task GetVM(TModel? model = null, string? errorMessage = null) where TModel : class + { + var externalProviders = await SignInManager.GetExternalProvidersAsync(); + + var request = HttpContext.Request; + + var result = new SetupVM + { + BaseUrlConfigured = urlGenerator.BuildUrl(), + BaseUrlCurrent = $"{request.Scheme}://{request.Host}", + ErrorMessage = errorMessage, + EverybodyCanCreateApps = !uiOptions.OnlyAdminsCanCreateApps, + IsValidHttps = HttpContext.Request.IsHttps, + IsAssetStoreFile = assetStore is FolderAssetStore, + IsAssetStoreFtp = assetStore is FTPAssetStore, + HasExternalLogin = externalProviders.Any(), + HasPasswordAuth = identityOptions.AllowPasswordAuth, + }; + + if (model != null) + { + SimpleMapper.Map(model, result); + } + + return result; + } + } +} diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Setup/SetupVM.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Setup/SetupVM.cs new file mode 100644 index 000000000..228db32cb --- /dev/null +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Setup/SetupVM.cs @@ -0,0 +1,32 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Areas.IdentityServer.Controllers.Setup +{ + public sealed class SetupVM + { + public string Email { get; set; } + + public string BaseUrlCurrent { get; set; } + + public string BaseUrlConfigured { get; set; } + + public string? ErrorMessage { get; set; } + + public bool IsValidHttps { get; set; } + + public bool IsAssetStoreFtp { get; set; } + + public bool IsAssetStoreFile { get; set; } + + public bool EverybodyCanCreateApps { get; set; } + + public bool HasExternalLogin { get; set; } + + public bool HasPasswordAuth { get; set; } + } +} diff --git a/backend/src/Squidex/Areas/IdentityServer/Views/Account/AccessDenied.cshtml b/backend/src/Squidex/Areas/IdentityServer/Views/Account/AccessDenied.cshtml index 07cf868e1..e158ff8b1 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Views/Account/AccessDenied.cshtml +++ b/backend/src/Squidex/Areas/IdentityServer/Views/Account/AccessDenied.cshtml @@ -1,5 +1,5 @@ @{ - ViewBag.Theme = "white"; + ViewBag.ThemeColor = "white"; ViewBag.Title = T.Get("users.accessDenied.title"); } diff --git a/backend/src/Squidex/Areas/IdentityServer/Views/Account/Consent.cshtml b/backend/src/Squidex/Areas/IdentityServer/Views/Account/Consent.cshtml index 319f720f0..ae8c1f74c 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Views/Account/Consent.cshtml +++ b/backend/src/Squidex/Areas/IdentityServer/Views/Account/Consent.cshtml @@ -1,7 +1,8 @@ @model Squidex.Areas.IdentityServer.Controllers.Account.ConsentVM @{ - ViewBag.Class = "profile-lg"; + ViewBag.ThemeColor = "white"; + ViewBag.ThemeSize = "profile-lg"; ViewBag.Title = T.Get("users.consent.title"); } diff --git a/backend/src/Squidex/Areas/IdentityServer/Views/Account/LockedOut.cshtml b/backend/src/Squidex/Areas/IdentityServer/Views/Account/LockedOut.cshtml index 6b48a258d..bd2cb8400 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Views/Account/LockedOut.cshtml +++ b/backend/src/Squidex/Areas/IdentityServer/Views/Account/LockedOut.cshtml @@ -1,5 +1,5 @@ @{ - ViewBag.Theme = "white"; + ViewBag.ThemeColor = "white"; ViewBag.Title = T.Get("users.lockedOutTitle"); } diff --git a/backend/src/Squidex/Areas/IdentityServer/Views/Account/Login.cshtml b/backend/src/Squidex/Areas/IdentityServer/Views/Account/Login.cshtml index 22f17675e..669a6fa1e 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Views/Account/Login.cshtml +++ b/backend/src/Squidex/Areas/IdentityServer/Views/Account/Login.cshtml @@ -1,11 +1,14 @@ @model Squidex.Areas.IdentityServer.Controllers.Account.LoginVM @{ + ViewBag.ThemeColor = "white"; + ViewBag.ThemeSize = "profile-lg"; + var action = Model.IsLogin ? T.Get("common.login") : T.Get("common.signup"); ViewBag.Title = action; } - +
@@ -42,7 +45,7 @@ } -@if (Model.HasPasswordAndExternal) +@if (Model.HasExternalLogin && Model.HasPasswordAuth) {
@T.Get("users.login.separator")
@@ -51,36 +54,36 @@ @if (Model.HasPasswordAuth) { - if (Model.IsLogin) + if (Model.IsLogin) + { + if (Model.IsFailed) { - if (Model.IsFailed) - { -
@T.Get("users.login.error")
- } +
@T.Get("users.login.error")
+ } -
-
- -
+ +
+ +
-
- -
+
+ +
- -
- } - else - { - - } + + + } + else + { + + } } @if (Model.IsLogin) {
} -
+
@@ -251,7 +252,7 @@
-
diff --git a/backend/src/Squidex/Areas/IdentityServer/Views/Setup/Setup.cshtml b/backend/src/Squidex/Areas/IdentityServer/Views/Setup/Setup.cshtml new file mode 100644 index 000000000..69a853003 --- /dev/null +++ b/backend/src/Squidex/Areas/IdentityServer/Views/Setup/Setup.cshtml @@ -0,0 +1,201 @@ +@model Squidex.Areas.IdentityServer.Controllers.Setup.SetupVM + +@{ + ViewBag.ThemeColor = "gray"; + ViewBag.ThemeSize = "profile-lg"; + + ViewBag.Title = T.Get("setup.title"); + + void RenderValidation(string field) + { + @if (ViewContext.ViewData.ModelState[field]?.ValidationState == Microsoft.AspNetCore.Mvc.ModelBinding.ModelValidationState.Invalid) + { +
+ Html.ValidationMessage(field) +
+ } + } + + void RenderRuleAsSuccess(string message) + { +
+
+
+ +
+
+ +
+
+ @Html.Raw(message) +
+
+
+ } + + void RenderRuleAsCritical(string message) + { +
+
+
+ +
+
+ +
+
+ @T.Get("common.critical"): @Html.Raw(message) +
+
+
+ } + + void RenderRuleAsWarning(string message) + { +
+
+
+ +
+
+ +
+
+ @T.Get("common.warning"): @Html.Raw(message) +
+
+
+ } +} + +
+
+

@T.Get("setup.headline")

+ + + + @T.Get("setup.hint") + +
+

@T.Get("setup.rules.headline")

+ + @if (Model.IsValidHttps) + { + RenderRuleAsSuccess(T.Get("setup.ruleHttps.success")); + } + else + { + RenderRuleAsCritical(T.Get("setup.ruleHttps.failure")); + } + + @if (Model.BaseUrlConfigured == Model.BaseUrlCurrent) + { + RenderRuleAsSuccess(T.Get("setup.ruleUrl.success")); + } + else + { + RenderRuleAsCritical(T.Get("setup.ruleUrl.failure", new { actual = Model.BaseUrlCurrent, configured = Model.BaseUrlConfigured })); + } + + @if (Model.EverybodyCanCreateApps) + { + RenderRuleAsWarning(T.Get("setup.ruleAppCreation.warningAdmins")); + } + else + { + RenderRuleAsWarning(T.Get("setup.ruleAppCreation.warningAll")); + } + + @if (Model.IsAssetStoreFtp) + { + RenderRuleAsWarning(T.Get("setup.ruleFtp.warning")); + } + + @if (Model.IsAssetStoreFile) + { + RenderRuleAsWarning(T.Get("setup.ruleFolder.warning")); + } +
+ +
+ +
+

@T.Get("setup.createUser.headline")

+ + @if (Model.HasExternalLogin) + { +
+ @T.Get("setup.createUser.loginHint") + + +
+ } + + @if (Model.HasExternalLogin && Model.HasPasswordAuth) + { +
+
@T.Get("setup.createUser.separator")
+
+ } + + @if (Model.HasPasswordAuth) + { +

@T.Get("setup.createUser.headlineCreate")

+ + @if (!string.IsNullOrWhiteSpace(Model.ErrorMessage)) + { +
+ @Model.ErrorMessage +
+ } + +
+
+ + + @{ RenderValidation("Email"); } + + +
+ +
+ + + @{ RenderValidation("Password"); } + + +
+ +
+ + + @{ RenderValidation("PasswordConfirm"); } + + +
+ +
+ +
+
+ } + + @if (!Model.HasExternalLogin && !Model.HasPasswordAuth) + { +
+ @T.Get("setup.createUser.failure") +
+ } +
+
+
+ +
+ + @T.Get("setup.madeBy")
@T.Get("setup.madeByCopyright") +
+
diff --git a/backend/src/Squidex/Areas/IdentityServer/Views/_Layout.cshtml b/backend/src/Squidex/Areas/IdentityServer/Views/_Layout.cshtml index 3b03dfa0b..437769bf1 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Views/_Layout.cshtml +++ b/backend/src/Squidex/Areas/IdentityServer/Views/_Layout.cshtml @@ -17,8 +17,8 @@ @await RenderSectionAsync("header") } - -
+ +
@RenderBody() diff --git a/backend/src/Squidex/Config/Web/WebServices.cs b/backend/src/Squidex/Config/Web/WebServices.cs index 825ee67b6..748ea7eff 100644 --- a/backend/src/Squidex/Config/Web/WebServices.cs +++ b/backend/src/Squidex/Config/Web/WebServices.cs @@ -51,21 +51,9 @@ namespace Squidex.Config.Web services.AddSingletonAs() .AsSelf(); - services.AddSingletonAs() - .AsSelf(); - - services.AddSingletonAs() - .AsSelf(); - services.AddSingletonAs() .AsSelf(); - services.AddSingletonAs() - .AsSelf(); - - services.AddSingletonAs() - .AsSelf(); - services.AddSingletonAs(c => translator) .As(); diff --git a/backend/src/Squidex/Pipeline/Robots/RobotsTxtMiddleware.cs b/backend/src/Squidex/Pipeline/Robots/RobotsTxtMiddleware.cs index 92ed64692..163c782e2 100644 --- a/backend/src/Squidex/Pipeline/Robots/RobotsTxtMiddleware.cs +++ b/backend/src/Squidex/Pipeline/Robots/RobotsTxtMiddleware.cs @@ -8,29 +8,28 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; -using Squidex.Infrastructure; namespace Squidex.Pipeline.Robots { - public sealed class RobotsTxtMiddleware : IMiddleware + public sealed class RobotsTxtMiddleware { - private readonly RobotsTxtOptions robotsTxtOptions; + private readonly RequestDelegate next; - public RobotsTxtMiddleware(IOptions robotsTxtOptions) + public RobotsTxtMiddleware(RequestDelegate next) { - Guard.NotNull(robotsTxtOptions, nameof(robotsTxtOptions)); - - this.robotsTxtOptions = robotsTxtOptions.Value; + this.next = next; } - public async Task InvokeAsync(HttpContext context, RequestDelegate next) + public async Task InvokeAsync(HttpContext context, IOptions robotsTxtOptions) { - if (CanServeRequest(context.Request) && !string.IsNullOrWhiteSpace(robotsTxtOptions.Text)) + var text = robotsTxtOptions.Value.Text; + + if (CanServeRequest(context.Request) && !string.IsNullOrWhiteSpace(text)) { context.Response.ContentType = "text/plain"; context.Response.StatusCode = 200; - await context.Response.WriteAsync(robotsTxtOptions.Text); + await context.Response.WriteAsync(text); } else { diff --git a/backend/src/Squidex/Pipeline/Squid/SquidMiddleware.cs b/backend/src/Squidex/Pipeline/Squid/SquidMiddleware.cs index d129234cf..620baf2fd 100644 --- a/backend/src/Squidex/Pipeline/Squid/SquidMiddleware.cs +++ b/backend/src/Squidex/Pipeline/Squid/SquidMiddleware.cs @@ -15,7 +15,6 @@ namespace Squidex.Pipeline.Squid { public sealed class SquidMiddleware { - private readonly RequestDelegate next; private readonly string squidHappyLG = LoadSvg("happy"); private readonly string squidHappySM = LoadSvg("happy-sm"); private readonly string squidSadLG = LoadSvg("sad"); @@ -23,76 +22,68 @@ namespace Squidex.Pipeline.Squid public SquidMiddleware(RequestDelegate next) { - this.next = next; } public async Task InvokeAsync(HttpContext context) { var request = context.Request; - if (request.Path.Equals("/squid.svg")) - { - var face = "sad"; - - if (request.Query.TryGetValue("face", out var faceValue) && (faceValue == "sad" || faceValue == "happy")) - { - face = faceValue; - } + var face = "sad"; - var isSad = face == "sad"; - - var title = isSad ? "OH DAMN!" : "OH YEAH!"; - - if (request.Query.TryGetValue("title", out var titleValue) && !string.IsNullOrWhiteSpace(titleValue)) - { - title = titleValue; - } + if (request.Query.TryGetValue("face", out var faceValue) && (faceValue == "sad" || faceValue == "happy")) + { + face = faceValue; + } - var text = "text"; + var isSad = face == "sad"; - if (request.Query.TryGetValue("text", out var textValue) && !string.IsNullOrWhiteSpace(textValue)) - { - text = textValue; - } + var title = isSad ? "OH DAMN!" : "OH YEAH!"; - var background = isSad ? "#F5F5F9" : "#4CC159"; - - if (request.Query.TryGetValue("background", out var backgroundValue) && !string.IsNullOrWhiteSpace(backgroundValue)) - { - background = backgroundValue; - } + if (request.Query.TryGetValue("title", out var titleValue) && !string.IsNullOrWhiteSpace(titleValue)) + { + title = titleValue; + } - var isSmall = request.Query.TryGetValue("small", out _); + var text = "text"; - string svg; + if (request.Query.TryGetValue("text", out var textValue) && !string.IsNullOrWhiteSpace(textValue)) + { + text = textValue; + } - if (isSmall) - { - svg = isSad ? squidSadSM : squidHappySM; - } - else - { - svg = isSad ? squidSadLG : squidHappyLG; - } + var background = isSad ? "#F5F5F9" : "#4CC159"; - var (l1, l2, l3) = SplitText(text); + if (request.Query.TryGetValue("background", out var backgroundValue) && !string.IsNullOrWhiteSpace(backgroundValue)) + { + background = backgroundValue; + } - svg = svg.Replace("{{TITLE}}", title.ToUpperInvariant()); - svg = svg.Replace("{{TEXT1}}", l1); - svg = svg.Replace("{{TEXT2}}", l2); - svg = svg.Replace("{{TEXT3}}", l3); - svg = svg.Replace("[COLOR]", background); + var isSmall = request.Query.TryGetValue("small", out _); - context.Response.StatusCode = 200; - context.Response.ContentType = "image/svg+xml"; - context.Response.Headers["Cache-Control"] = "public, max-age=604800"; + string svg; - await context.Response.WriteAsync(svg); + if (isSmall) + { + svg = isSad ? squidSadSM : squidHappySM; } else { - await next(context); + svg = isSad ? squidSadLG : squidHappyLG; } + + var (l1, l2, l3) = SplitText(text); + + svg = svg.Replace("{{TITLE}}", title.ToUpperInvariant()); + svg = svg.Replace("{{TEXT1}}", l1); + svg = svg.Replace("{{TEXT2}}", l2); + svg = svg.Replace("{{TEXT3}}", l3); + svg = svg.Replace("[COLOR]", background); + + context.Response.StatusCode = 200; + context.Response.ContentType = "image/svg+xml"; + context.Response.Headers["Cache-Control"] = "public, max-age=604800"; + + await context.Response.WriteAsync(svg); } private static (string, string, string) SplitText(string text) diff --git a/backend/tests/Squidex.Web.Tests/Pipeline/RequestExceptionMiddlewareTests.cs b/backend/tests/Squidex.Web.Tests/Pipeline/RequestExceptionMiddlewareTests.cs index 2fb6f31a3..2339d07a3 100644 --- a/backend/tests/Squidex.Web.Tests/Pipeline/RequestExceptionMiddlewareTests.cs +++ b/backend/tests/Squidex.Web.Tests/Pipeline/RequestExceptionMiddlewareTests.cs @@ -24,7 +24,6 @@ namespace Squidex.Web.Pipeline private readonly IHttpResponseFeature responseFeature = A.Fake(); private readonly HttpContext httpContext = new DefaultHttpContext(); private readonly RequestDelegate next; - private readonly RequestExceptionMiddleware sut; private bool isNextCalled; public RequestExceptionMiddlewareTests() @@ -37,8 +36,6 @@ namespace Squidex.Web.Pipeline }; httpContext.Features.Set(responseFeature); - - sut = new RequestExceptionMiddleware(resultWriter, log); } [Fact] @@ -46,7 +43,9 @@ namespace Squidex.Web.Pipeline { httpContext.Request.QueryString = new QueryString("?error=412"); - await sut.InvokeAsync(httpContext, next); + var sut = new RequestExceptionMiddleware(next); + + await sut.InvokeAsync(httpContext, resultWriter, log); Assert.False(isNextCalled); @@ -60,7 +59,9 @@ namespace Squidex.Web.Pipeline { httpContext.Request.QueryString = new QueryString("?error=hello"); - await sut.InvokeAsync(httpContext, next); + var sut = new RequestExceptionMiddleware(next); + + await sut.InvokeAsync(httpContext, resultWriter, log); Assert.True(isNextCalled); @@ -73,7 +74,9 @@ namespace Squidex.Web.Pipeline { httpContext.Request.QueryString = new QueryString("?error=99"); - await sut.InvokeAsync(httpContext, next); + var sut = new RequestExceptionMiddleware(next); + + await sut.InvokeAsync(httpContext, resultWriter, log); Assert.True(isNextCalled); @@ -89,7 +92,9 @@ namespace Squidex.Web.Pipeline throw new InvalidOperationException(); }); - await sut.InvokeAsync(httpContext, failingNext); + var sut = new RequestExceptionMiddleware(failingNext); + + await sut.InvokeAsync(httpContext, resultWriter, log); A.CallTo(() => resultWriter.ExecuteAsync(A._, A.That.Matches(x => x.StatusCode == 500 && x.Value is ErrorDto))) @@ -106,7 +111,9 @@ namespace Squidex.Web.Pipeline throw ex; }); - await sut.InvokeAsync(httpContext, failingNext); + var sut = new RequestExceptionMiddleware(failingNext); + + await sut.InvokeAsync(httpContext, resultWriter, log); A.CallTo(() => log.Log(SemanticLogLevel.Error, ex, A._)) .MustHaveHappened(); @@ -123,7 +130,9 @@ namespace Squidex.Web.Pipeline throw new InvalidOperationException(); }); - await sut.InvokeAsync(httpContext, failingNext); + var sut = new RequestExceptionMiddleware(failingNext); + + await sut.InvokeAsync(httpContext, resultWriter, log); A.CallTo(() => resultWriter.ExecuteAsync(A._, A._)) .MustNotHaveHappened(); @@ -139,7 +148,9 @@ namespace Squidex.Web.Pipeline return Task.CompletedTask; }); - await sut.InvokeAsync(httpContext, failingNext); + var sut = new RequestExceptionMiddleware(failingNext); + + await sut.InvokeAsync(httpContext, resultWriter, log); A.CallTo(() => resultWriter.ExecuteAsync(A._, A.That.Matches(x => x.StatusCode == 412 && x.Value is ErrorDto))) @@ -159,7 +170,9 @@ namespace Squidex.Web.Pipeline return Task.CompletedTask; }); - await sut.InvokeAsync(httpContext, failingNext); + var sut = new RequestExceptionMiddleware(failingNext); + + await sut.InvokeAsync(httpContext, resultWriter, log); A.CallTo(() => resultWriter.ExecuteAsync(A._, A._)) .MustNotHaveHappened(); diff --git a/frontend/app/framework/angular/status-icon.component.html b/frontend/app/framework/angular/status-icon.component.html index 92f203ff9..e93f30eb2 100644 --- a/frontend/app/framework/angular/status-icon.component.html +++ b/frontend/app/framework/angular/status-icon.component.html @@ -1,14 +1,14 @@ -
+
-
+
-
+
-
+
\ No newline at end of file diff --git a/frontend/app/framework/angular/status-icon.component.scss b/frontend/app/framework/angular/status-icon.component.scss index 3ca50c75f..e69de29bb 100644 --- a/frontend/app/framework/angular/status-icon.component.scss +++ b/frontend/app/framework/angular/status-icon.component.scss @@ -1,28 +0,0 @@ -.status { - & { - background: $color-border; - border: 0; - color: $color-dark-foreground; - display: inline-block; - } - - &.sm { - @include circle-icon(1.6rem); - } - - &.lg { - @include circle-icon(2.8rem); - } - - &-pending { - color: inherit; - } - - &-failed { - background: $color-theme-error; - } - - &-success { - background: $color-theme-green; - } -} \ No newline at end of file diff --git a/frontend/app/shell/pages/home/home-page.component.html b/frontend/app/shell/pages/home/home-page.component.html index c42439d53..7604ef21f 100644 --- a/frontend/app/shell/pages/home/home-page.component.html +++ b/frontend/app/shell/pages/home/home-page.component.html @@ -17,7 +17,9 @@
-
- {{ 'start.madeBy' | sqxTranslate }}
{{ 'start.madeByCopyright' | sqxTranslate }} +
+ + {{ 'start.madeBy' | sqxTranslate }}
{{ 'start.madeByCopyright' | sqxTranslate }} +
diff --git a/frontend/app/shell/pages/home/home-page.component.scss b/frontend/app/shell/pages/home/home-page.component.scss index 6380c882c..b97f10ed3 100644 --- a/frontend/app/shell/pages/home/home-page.component.scss +++ b/frontend/app/shell/pages/home/home-page.component.scss @@ -34,13 +34,4 @@ &-element { margin-top: 2rem; } -} - -.proudly-made { - color: $color-text-decent; - font-size: .9rem; - font-style: normal; - line-height: 1.5rem; - margin: 2rem 6rem; - text-align: center; } \ No newline at end of file diff --git a/frontend/app/theme/_common.scss b/frontend/app/theme/_common.scss index 482fe579c..028f9161a 100644 --- a/frontend/app/theme/_common.scss +++ b/frontend/app/theme/_common.scss @@ -215,6 +215,43 @@ body { } } +// +// Status Icon +// +.status-icon { + & { + @include circle-icon(2rem); + background: $color-border; + border: 0; + color: $color-dark-foreground; + display: inline-block; + } + + &-sm { + @include circle-icon(1.6rem); + } + + &-lg { + @include circle-icon(2.8rem); + } + + &-pending { + color: inherit; + } + + &-warning { + background: $color-theme-orange; + } + + &-failed { + background: $color-theme-error; + } + + &-success { + background: $color-theme-green; + } +} + // // Animations // diff --git a/frontend/app/theme/_static.scss b/frontend/app/theme/_static.scss index 31238cec0..9b230bfb4 100644 --- a/frontend/app/theme/_static.scss +++ b/frontend/app/theme/_static.scss @@ -42,7 +42,7 @@ noscript { } &-section { - margin-top: 2rem; + margin-top: 3rem; } &-section-sm {