Browse Source

Setup guide.

pull/625/head
Sebastian 5 years ago
parent
commit
dd0247956b
  1. 2
      backend/i18n/frontend_en.json
  2. 17
      backend/i18n/source/backend_en.json
  3. 2
      backend/i18n/source/frontend_en.json
  4. 2
      backend/src/Squidex.Domain.Users/DefaultUserService.cs
  5. 51
      backend/src/Squidex.Shared/Texts.it.resx
  6. 51
      backend/src/Squidex.Shared/Texts.nl.resx
  7. 51
      backend/src/Squidex.Shared/Texts.resx
  8. 13
      backend/src/Squidex.Web/Pipeline/LocalCacheMiddleware.cs
  9. 27
      backend/src/Squidex.Web/Pipeline/RequestExceptionMiddleware.cs
  10. 13
      backend/src/Squidex.Web/Pipeline/RequestLogPerformanceMiddleware.cs
  11. 38
      backend/src/Squidex.Web/Pipeline/SetupMiddleware.cs
  12. 11
      backend/src/Squidex.Web/Pipeline/UsageMiddleware.cs
  13. 23
      backend/src/Squidex/Areas/Frontend/Startup.cs
  14. 35
      backend/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs
  15. 8
      backend/src/Squidex/Areas/IdentityServer/Controllers/Error/ErrorController.cs
  16. 2
      backend/src/Squidex/Areas/IdentityServer/Controllers/Error/ErrorVM.cs
  17. 36
      backend/src/Squidex/Areas/IdentityServer/Controllers/IdentityServerController.cs
  18. 31
      backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs
  19. 23
      backend/src/Squidex/Areas/IdentityServer/Controllers/Setup/CreateUserModel.cs
  20. 115
      backend/src/Squidex/Areas/IdentityServer/Controllers/Setup/SetupController.cs
  21. 28
      backend/src/Squidex/Areas/IdentityServer/Controllers/Setup/SetupVM.cs
  22. 2
      backend/src/Squidex/Areas/IdentityServer/Views/Account/AccessDenied.cshtml
  23. 2
      backend/src/Squidex/Areas/IdentityServer/Views/Account/Consent.cshtml
  24. 2
      backend/src/Squidex/Areas/IdentityServer/Views/Account/LockedOut.cshtml
  25. 2
      backend/src/Squidex/Areas/IdentityServer/Views/Account/Login.cshtml
  26. 2
      backend/src/Squidex/Areas/IdentityServer/Views/Account/LogoutCompleted.cshtml
  27. 4
      backend/src/Squidex/Areas/IdentityServer/Views/Error/Error.cshtml
  28. 3
      backend/src/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml
  29. 167
      backend/src/Squidex/Areas/IdentityServer/Views/Setup/Setup.cshtml
  30. 4
      backend/src/Squidex/Areas/IdentityServer/Views/_Layout.cshtml
  31. 12
      backend/src/Squidex/Config/Web/WebServices.cs
  32. 19
      backend/src/Squidex/Pipeline/Robots/RobotsTxtMiddleware.cs
  33. 91
      backend/src/Squidex/Pipeline/Squid/SquidMiddleware.cs
  34. 8
      frontend/app/framework/angular/status-icon.component.html
  35. 28
      frontend/app/framework/angular/status-icon.component.scss
  36. 6
      frontend/app/shell/pages/home/home-page.component.html
  37. 9
      frontend/app/shell/pages/home/home-page.component.scss
  38. 33
      frontend/app/theme/_common.scss

2
backend/i18n/frontend_en.json

@ -851,7 +851,7 @@
"start.login": "Login to Squidex", "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.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.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.joinForum": "Join our Forum",
"tour.joinGithub": "Join us on Github", "tour.joinGithub": "Join us on Github",
"tour.skip": "Skip Tour", "tour.skip": "Skip Tour",

17
backend/i18n/source/backend_en.json

@ -269,6 +269,23 @@
"search.contentsResult": "{name} Contents", "search.contentsResult": "{name} Contents",
"search.schemaResult": "{name} Schema", "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!", "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": "One Time 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.https.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 <code>X-Forwarded-*</code> headers.",
"setup.https.success": "Congratulations, you are accessing your Squidex installation over a secure connection (https).",
"setup.madeBy": "Proudly made by",
"setup.madeByCopyright": "Sebastian Stehle and Contributors, 2016-2021",
"setup.title": "Installation",
"setup.url.failure": "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>.",
"setup.url.success": "Congratulations the <code>URLS__BASEURL</code> environment variable is configured properly.",
"users.accessDenied.text": "This operation is not allowed, your account might be locked.", "users.accessDenied.text": "This operation is not allowed, your account might be locked.",
"users.accessDenied.title": "Access denied", "users.accessDenied.title": "Access denied",
"users.consent.agree": "I agree!", "users.consent.agree": "I agree!",

2
backend/i18n/source/frontend_en.json

@ -851,7 +851,7 @@
"start.login": "Login to Squidex", "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.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.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.joinForum": "Join our Forum",
"tour.joinGithub": "Join us on Github", "tour.joinGithub": "Join us on Github",
"tour.skip": "Skip Tour", "tour.skip": "Skip Tour",

2
backend/src/Squidex.Domain.Users/DefaultUserService.cs

@ -44,7 +44,7 @@ namespace Squidex.Domain.Users
public async Task<bool> IsEmptyAsync() public async Task<bool> IsEmptyAsync()
{ {
var result = await QueryAsync(null, 0, 0); var result = await QueryAsync(null, 1, 0);
return result.Total == 0; return result.Total == 0;
} }

51
backend/src/Squidex.Shared/Texts.it.resx

@ -892,6 +892,57 @@
<data name="security.passwordStolen" xml:space="preserve"> <data name="security.passwordStolen" xml:space="preserve">
<value>Questa password risulta essere stata compromessa e non dovrebbe essere mai utilizzata. Se l'hai utilizzata in precedenza, cambiala!</value> <value>Questa password risulta essere stata compromessa e non dovrebbe essere mai utilizzata. Se l'hai utilizzata in precedenza, cambiala!</value>
</data> </data>
<data name="setup.createUser.button" xml:space="preserve">
<value>Create User</value>
</data>
<data name="setup.createUser.confirmPassword" xml:space="preserve">
<value>Confirm</value>
</data>
<data name="setup.createUser.failure" xml:space="preserve">
<value>Neither password authentication nor an external authentication provider such as Google is configured. Please check your settings and logs.</value>
</data>
<data name="setup.createUser.headline" xml:space="preserve">
<value>Admin User</value>
</data>
<data name="setup.createUser.headlineCreate" xml:space="preserve">
<value>Create Admin User</value>
</data>
<data name="setup.createUser.loginHint" xml:space="preserve">
<value>You have configured at least one external authentication provider such as Google. Just go to the login page and login to become administrator.</value>
</data>
<data name="setup.createUser.loginLink" xml:space="preserve">
<value>Go to Login Page.</value>
</data>
<data name="setup.createUser.separator" xml:space="preserve">
<value>OR</value>
</data>
<data name="setup.headline" xml:space="preserve">
<value>One Time Installation</value>
</data>
<data name="setup.hint" xml:space="preserve">
<value>You are seeing this screen because no user exists yet. After a user is created you are not able to use this screen again.</value>
</data>
<data name="setup.https.failure" xml:space="preserve">
<value> 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 &lt;code&gt;X-Forwarded-*&lt;/code&gt; headers.</value>
</data>
<data name="setup.https.success" xml:space="preserve">
<value>Congratulations, you are accessing your Squidex installation over a secure connection (https).</value>
</data>
<data name="setup.madeBy" xml:space="preserve">
<value>Proudly made by</value>
</data>
<data name="setup.madeByCopyright" xml:space="preserve">
<value>Sebastian Stehle and Contributors, 2016-2021</value>
</data>
<data name="setup.title" xml:space="preserve">
<value>Installation</value>
</data>
<data name="setup.url.failure" xml:space="preserve">
<value>You should access Squidex only over one one canonical URL and configure this URL over the &lt;code&gt;URLS__BASEURL&lt;/code&gt; environment variable. The current base URL &lt;code&gt;{actual}&lt;/code&gt; does not match to the base url &lt;code&gt;{configured}&lt;/code&gt;.</value>
</data>
<data name="setup.url.success" xml:space="preserve">
<value>Congratulations the &lt;code&gt;URLS__BASEURL&lt;/code&gt; environment variable is configured properly.</value>
</data>
<data name="users.accessDenied.text" xml:space="preserve"> <data name="users.accessDenied.text" xml:space="preserve">
<value>Questa operazione non è consentita, il tuo account potrebbe essere bloccato.</value> <value>Questa operazione non è consentita, il tuo account potrebbe essere bloccato.</value>
</data> </data>

51
backend/src/Squidex.Shared/Texts.nl.resx

@ -892,6 +892,57 @@
<data name="security.passwordStolen" xml:space="preserve"> <data name="security.passwordStolen" xml:space="preserve">
<value>Dit wachtwoord is eerder verschenen bij een datalek en mag nooit worden gebruikt. Als je het ooit eerder hebt gebruikt, verander het dan!</value> <value>Dit wachtwoord is eerder verschenen bij een datalek en mag nooit worden gebruikt. Als je het ooit eerder hebt gebruikt, verander het dan!</value>
</data> </data>
<data name="setup.createUser.button" xml:space="preserve">
<value>Create User</value>
</data>
<data name="setup.createUser.confirmPassword" xml:space="preserve">
<value>Confirm</value>
</data>
<data name="setup.createUser.failure" xml:space="preserve">
<value>Neither password authentication nor an external authentication provider such as Google is configured. Please check your settings and logs.</value>
</data>
<data name="setup.createUser.headline" xml:space="preserve">
<value>Admin User</value>
</data>
<data name="setup.createUser.headlineCreate" xml:space="preserve">
<value>Create Admin User</value>
</data>
<data name="setup.createUser.loginHint" xml:space="preserve">
<value>You have configured at least one external authentication provider such as Google. Just go to the login page and login to become administrator.</value>
</data>
<data name="setup.createUser.loginLink" xml:space="preserve">
<value>Go to Login Page.</value>
</data>
<data name="setup.createUser.separator" xml:space="preserve">
<value>OR</value>
</data>
<data name="setup.headline" xml:space="preserve">
<value>One Time Installation</value>
</data>
<data name="setup.hint" xml:space="preserve">
<value>You are seeing this screen because no user exists yet. After a user is created you are not able to use this screen again.</value>
</data>
<data name="setup.https.failure" xml:space="preserve">
<value> 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 &lt;code&gt;X-Forwarded-*&lt;/code&gt; headers.</value>
</data>
<data name="setup.https.success" xml:space="preserve">
<value>Congratulations, you are accessing your Squidex installation over a secure connection (https).</value>
</data>
<data name="setup.madeBy" xml:space="preserve">
<value>Proudly made by</value>
</data>
<data name="setup.madeByCopyright" xml:space="preserve">
<value>Sebastian Stehle and Contributors, 2016-2021</value>
</data>
<data name="setup.title" xml:space="preserve">
<value>Installation</value>
</data>
<data name="setup.url.failure" xml:space="preserve">
<value>You should access Squidex only over one one canonical URL and configure this URL over the &lt;code&gt;URLS__BASEURL&lt;/code&gt; environment variable. The current base URL &lt;code&gt;{actual}&lt;/code&gt; does not match to the base url &lt;code&gt;{configured}&lt;/code&gt;.</value>
</data>
<data name="setup.url.success" xml:space="preserve">
<value>Congratulations the &lt;code&gt;URLS__BASEURL&lt;/code&gt; environment variable is configured properly.</value>
</data>
<data name="users.accessDenied.text" xml:space="preserve"> <data name="users.accessDenied.text" xml:space="preserve">
<value>Deze bewerking is niet toegestaan, je account is mogelijk vergrendeld.</value> <value>Deze bewerking is niet toegestaan, je account is mogelijk vergrendeld.</value>
</data> </data>

51
backend/src/Squidex.Shared/Texts.resx

@ -892,6 +892,57 @@
<data name="security.passwordStolen" xml:space="preserve"> <data name="security.passwordStolen" xml:space="preserve">
<value>This password has previously appeared in a data breach and should never be used. If you have ever used it anywhere before, change it!</value> <value>This password has previously appeared in a data breach and should never be used. If you have ever used it anywhere before, change it!</value>
</data> </data>
<data name="setup.createUser.button" xml:space="preserve">
<value>Create User</value>
</data>
<data name="setup.createUser.confirmPassword" xml:space="preserve">
<value>Confirm</value>
</data>
<data name="setup.createUser.failure" xml:space="preserve">
<value>Neither password authentication nor an external authentication provider such as Google is configured. Please check your settings and logs.</value>
</data>
<data name="setup.createUser.headline" xml:space="preserve">
<value>Admin User</value>
</data>
<data name="setup.createUser.headlineCreate" xml:space="preserve">
<value>Create Admin User</value>
</data>
<data name="setup.createUser.loginHint" xml:space="preserve">
<value>You have configured at least one external authentication provider such as Google. Just go to the login page and login to become administrator.</value>
</data>
<data name="setup.createUser.loginLink" xml:space="preserve">
<value>Go to Login Page.</value>
</data>
<data name="setup.createUser.separator" xml:space="preserve">
<value>OR</value>
</data>
<data name="setup.headline" xml:space="preserve">
<value>One Time Installation</value>
</data>
<data name="setup.hint" xml:space="preserve">
<value>You are seeing this screen because no user exists yet. After a user is created you are not able to use this screen again.</value>
</data>
<data name="setup.https.failure" xml:space="preserve">
<value> 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 &lt;code&gt;X-Forwarded-*&lt;/code&gt; headers.</value>
</data>
<data name="setup.https.success" xml:space="preserve">
<value>Congratulations, you are accessing your Squidex installation over a secure connection (https).</value>
</data>
<data name="setup.madeBy" xml:space="preserve">
<value>Proudly made by</value>
</data>
<data name="setup.madeByCopyright" xml:space="preserve">
<value>Sebastian Stehle and Contributors, 2016-2021</value>
</data>
<data name="setup.title" xml:space="preserve">
<value>Installation</value>
</data>
<data name="setup.url.failure" xml:space="preserve">
<value>You should access Squidex only over one one canonical URL and configure this URL over the &lt;code&gt;URLS__BASEURL&lt;/code&gt; environment variable. The current base URL &lt;code&gt;{actual}&lt;/code&gt; does not match to the base url &lt;code&gt;{configured}&lt;/code&gt;.</value>
</data>
<data name="setup.url.success" xml:space="preserve">
<value>Congratulations the &lt;code&gt;URLS__BASEURL&lt;/code&gt; environment variable is configured properly.</value>
</data>
<data name="users.accessDenied.text" xml:space="preserve"> <data name="users.accessDenied.text" xml:space="preserve">
<value>This operation is not allowed, your account might be locked.</value> <value>This operation is not allowed, your account might be locked.</value>
</data> </data>

13
backend/src/Squidex.Web/Pipeline/LocalCacheMiddleware.cs

@ -8,22 +8,19 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Squidex.Caching; using Squidex.Caching;
using Squidex.Infrastructure;
namespace Squidex.Web.Pipeline 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.next = next;
this.localCache = localCache;
} }
public async Task InvokeAsync(HttpContext context, RequestDelegate next) public async Task InvokeAsync(HttpContext context, ILocalCache localCache)
{ {
using (localCache.StartContext()) using (localCache.StartContext())
{ {

27
backend/src/Squidex.Web/Pipeline/RequestExceptionMiddleware.cs

@ -12,35 +12,28 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;
using Squidex.Infrastructure;
using Squidex.Log; using Squidex.Log;
namespace Squidex.Web.Pipeline namespace Squidex.Web.Pipeline
{ {
public sealed class RequestExceptionMiddleware : IMiddleware public sealed class RequestExceptionMiddleware
{ {
private static readonly ActionDescriptor EmptyActionDescriptor = new ActionDescriptor(); private static readonly ActionDescriptor EmptyActionDescriptor = new ActionDescriptor();
private static readonly RouteData EmptyRouteData = new RouteData(); private static readonly RouteData EmptyRouteData = new RouteData();
private readonly IActionResultExecutor<ObjectResult> resultWriter; private readonly RequestDelegate next;
private readonly ISemanticLog log;
public RequestExceptionMiddleware(IActionResultExecutor<ObjectResult> resultWriter, ISemanticLog log) public RequestExceptionMiddleware(RequestDelegate next)
{ {
Guard.NotNull(resultWriter, nameof(resultWriter)); this.next = next;
Guard.NotNull(log, nameof(log));
this.resultWriter = resultWriter;
this.log = log;
} }
public async Task InvokeAsync(HttpContext context, RequestDelegate next) public async Task InvokeAsync(HttpContext context, IActionResultExecutor<ObjectResult> writer, ISemanticLog log)
{ {
if (context.Request.Query.TryGetValue("error", out var header) && int.TryParse(header, out var statusCode) && IsErrorStatusCode(statusCode)) if (context.Request.Query.TryGetValue("error", out var header) && int.TryParse(header, out var statusCode) && IsErrorStatusCode(statusCode))
{ {
var (error, _) = ApiExceptionConverter.ToErrorDto(statusCode, context); var (error, _) = ApiExceptionConverter.ToErrorDto(statusCode, context);
await WriteErrorAsync(context, error); await WriteErrorAsync(context, error, writer);
return; return;
} }
@ -56,7 +49,7 @@ namespace Squidex.Web.Pipeline
{ {
var (error, _) = ex.ToErrorDto(context); 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); 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<ObjectResult> writer)
{ {
var actionRouteData = context.GetRouteData() ?? EmptyRouteData; var actionRouteData = context.GetRouteData() ?? EmptyRouteData;
var actionContext = new ActionContext(context, actionRouteData, EmptyActionDescriptor); var actionContext = new ActionContext(context, actionRouteData, EmptyActionDescriptor);
await resultWriter.ExecuteAsync(actionContext, new ObjectResult(error) await writer.ExecuteAsync(actionContext, new ObjectResult(error)
{ {
StatusCode = error.StatusCode StatusCode = error.StatusCode
}); });

13
backend/src/Squidex.Web/Pipeline/RequestLogPerformanceMiddleware.cs

@ -14,22 +14,19 @@ using Squidex.Log;
namespace Squidex.Web.Pipeline namespace Squidex.Web.Pipeline
{ {
public sealed class RequestLogPerformanceMiddleware : IMiddleware public sealed class RequestLogPerformanceMiddleware
{ {
private readonly RequestLogOptions requestLogOptions; private readonly RequestLogOptions requestLogOptions;
private readonly ISemanticLog log; private readonly RequestDelegate next;
public RequestLogPerformanceMiddleware(IOptions<RequestLogOptions> requestLogOptions, ISemanticLog log) public RequestLogPerformanceMiddleware(RequestDelegate next, IOptions<RequestLogOptions> requestLogOptions)
{ {
Guard.NotNull(requestLogOptions, nameof(requestLogOptions));
Guard.NotNull(log, nameof(log));
this.requestLogOptions = requestLogOptions.Value; 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(); var watch = ValueStopwatch.StartNew();

38
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);
}
}
}
}

11
backend/src/Squidex.Web/Pipeline/UsageMiddleware.cs

@ -18,18 +18,17 @@ namespace Squidex.Web.Pipeline
{ {
public sealed class UsageMiddleware : IMiddleware public sealed class UsageMiddleware : IMiddleware
{ {
private readonly IAppLogStore logStore; private readonly IAppLogStore usageLog;
private readonly IApiUsageTracker usageTracker; private readonly IApiUsageTracker usageTracker;
private readonly IClock clock; 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(usageTracker, nameof(usageTracker));
Guard.NotNull(clock, nameof(clock)); Guard.NotNull(clock, nameof(clock));
this.logStore = logStore; this.usageLog = usageLog;
this.usageTracker = usageTracker; this.usageTracker = usageTracker;
this.clock = clock; this.clock = clock;
@ -73,7 +72,7 @@ namespace Squidex.Web.Pipeline
request.UserClientId = clientId; request.UserClientId = clientId;
request.UserId = context.User.OpenIdSubject(); request.UserId = context.User.OpenIdSubject();
await logStore.LogAsync(appId.Id, request); await usageLog.LogAsync(appId.Id, request);
if (request.Costs > 0) if (request.Costs > 0)
{ {

23
backend/src/Squidex/Areas/Frontend/Startup.cs

@ -15,6 +15,7 @@ using Microsoft.Extensions.Hosting;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
using Squidex.Areas.Frontend.Middlewares; using Squidex.Areas.Frontend.Middlewares;
using Squidex.Pipeline.Squid; using Squidex.Pipeline.Squid;
using Squidex.Web.Pipeline;
namespace Squidex.Areas.Frontend namespace Squidex.Areas.Frontend
{ {
@ -24,9 +25,15 @@ namespace Squidex.Areas.Frontend
{ {
var environment = app.ApplicationServices.GetRequiredService<IWebHostEnvironment>(); var environment = app.ApplicationServices.GetRequiredService<IWebHostEnvironment>();
app.UseMiddleware<SquidMiddleware>(); app.Map("/squid.svg", builder => builder.UseMiddleware<SquidMiddleware>());
app.UseMiddleware<NotifoMiddleware>(); app.UseMiddleware<NotifoMiddleware>();
var indexFile =
environment.IsProduction() ?
new PathString("/build/index.html") :
new PathString("/index.html");
app.Use((context, next) => app.Use((context, next) =>
{ {
if (context.Request.Path == "/client-callback-popup") if (context.Request.Path == "/client-callback-popup")
@ -39,19 +46,17 @@ namespace Squidex.Areas.Frontend
} }
else if (!Path.HasExtension(context.Request.Path.Value)) else if (!Path.HasExtension(context.Request.Path.Value))
{ {
if (environment.IsDevelopment()) context.Request.Path = indexFile;
{
context.Request.Path = new PathString("/index.html");
}
else
{
context.Request.Path = new PathString("/build/index.html");
}
} }
return next(); return next();
}); });
app.UseWhen(x => x.Request.Path.StartsWithSegments(indexFile), builder =>
{
builder.UseMiddleware<SetupMiddleware>();
});
app.UseMiddleware<IndexMiddleware>(); app.UseMiddleware<IndexMiddleware>();
if (environment.IsDevelopment()) if (environment.IsDevelopment())

35
backend/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs

@ -31,7 +31,6 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account
{ {
public sealed class AccountController : IdentityServerController public sealed class AccountController : IdentityServerController
{ {
private readonly SignInManager<IdentityUser> signInManager;
private readonly IUserService userService; private readonly IUserService userService;
private readonly IUrlGenerator urlGenerator; private readonly IUrlGenerator urlGenerator;
private readonly MyIdentityOptions identityOptions; private readonly MyIdentityOptions identityOptions;
@ -39,7 +38,6 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account
private readonly IIdentityServerInteractionService interactions; private readonly IIdentityServerInteractionService interactions;
public AccountController( public AccountController(
SignInManager<IdentityUser> signInManager,
IUserService userService, IUserService userService,
IUrlGenerator urlGenerator, IUrlGenerator urlGenerator,
IOptions<MyIdentityOptions> identityOptions, IOptions<MyIdentityOptions> identityOptions,
@ -48,7 +46,6 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account
{ {
this.identityOptions = identityOptions.Value; this.identityOptions = identityOptions.Value;
this.interactions = interactions; this.interactions = interactions;
this.signInManager = signInManager;
this.urlGenerator = urlGenerator; this.urlGenerator = urlGenerator;
this.userService = userService; this.userService = userService;
this.log = log; this.log = log;
@ -139,7 +136,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account
[Route("account/logout/")] [Route("account/logout/")]
public async Task<IActionResult> Logout(string logoutId) public async Task<IActionResult> Logout(string logoutId)
{ {
await signInManager.SignOutAsync(); await SignInManager.SignOutAsync();
if (User.Identity?.IsAuthenticated == true) if (User.Identity?.IsAuthenticated == true)
{ {
@ -165,7 +162,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account
[Route("account/logout-redirect/")] [Route("account/logout-redirect/")]
public async Task<IActionResult> LogoutRedirect() public async Task<IActionResult> LogoutRedirect()
{ {
await signInManager.SignOutAsync(); await SignInManager.SignOutAsync();
return RedirectToAction(nameof(LogoutCompleted)); return RedirectToAction(nameof(LogoutCompleted));
} }
@ -194,7 +191,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account
return await LoginViewAsync(returnUrl, true, true); 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) if (!result.Succeeded && result.IsLockedOut)
{ {
@ -214,14 +211,14 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account
{ {
var allowPasswordAuth = identityOptions.AllowPasswordAuth; var allowPasswordAuth = identityOptions.AllowPasswordAuth;
var externalProviders = await signInManager.GetExternalProvidersAsync(); var externalProviders = await SignInManager.GetExternalProvidersAsync();
if (externalProviders.Count == 1 && !allowPasswordAuth) if (externalProviders.Count == 1 && !allowPasswordAuth)
{ {
var provider = externalProviders[0].AuthenticationScheme; var provider = externalProviders[0].AuthenticationScheme;
var properties = var properties =
signInManager.ConfigureExternalAuthenticationProperties(provider, SignInManager.ConfigureExternalAuthenticationProperties(provider,
Url.Action(nameof(ExternalCallback), new { ReturnUrl = returnUrl })); Url.Action(nameof(ExternalCallback), new { ReturnUrl = returnUrl }));
return Challenge(properties, provider); return Challenge(properties, provider);
@ -230,8 +227,8 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account
var vm = new LoginVM var vm = new LoginVM
{ {
ExternalProviders = externalProviders, ExternalProviders = externalProviders,
IsLogin = isLogin,
IsFailed = isFailed, IsFailed = isFailed,
IsLogin = isLogin,
HasPasswordAuth = allowPasswordAuth, HasPasswordAuth = allowPasswordAuth,
HasPasswordAndExternal = allowPasswordAuth && externalProviders.Any(), HasPasswordAndExternal = allowPasswordAuth && externalProviders.Any(),
ReturnUrl = returnUrl ReturnUrl = returnUrl
@ -245,7 +242,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account
public IActionResult External(string provider, string? returnUrl = null) public IActionResult External(string provider, string? returnUrl = null)
{ {
var properties = var properties =
signInManager.ConfigureExternalAuthenticationProperties(provider, SignInManager.ConfigureExternalAuthenticationProperties(provider,
Url.Action(nameof(ExternalCallback), new { ReturnUrl = returnUrl })); Url.Action(nameof(ExternalCallback), new { ReturnUrl = returnUrl }));
return Challenge(properties, provider); return Challenge(properties, provider);
@ -255,14 +252,14 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account
[Route("account/external-callback/")] [Route("account/external-callback/")]
public async Task<IActionResult> ExternalCallback(string? returnUrl = null) public async Task<IActionResult> ExternalCallback(string? returnUrl = null)
{ {
var externalLogin = await signInManager.GetExternalLoginInfoWithDisplayNameAsync(); var externalLogin = await SignInManager.GetExternalLoginInfoWithDisplayNameAsync();
if (externalLogin == null) if (externalLogin == null)
{ {
return RedirectToAction(nameof(Login)); 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) if (!result.Succeeded && result.IsLockedOut)
{ {
@ -349,7 +346,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account
private async Task<(bool Success, bool Locked)> LoginAsync(UserLoginInfo externalLogin) 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); return (result.Succeeded, result.IsLockedOut);
} }
@ -365,17 +362,5 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account
return Redirect("~/../"); return Redirect("~/../");
} }
} }
private IActionResult RedirectToReturnUrl(string? returnUrl)
{
if (urlGenerator.IsAllowedHost(returnUrl) || interactions.IsValidReturnUrl(returnUrl))
{
return Redirect(returnUrl);
}
else
{
return Redirect("~/../");
}
}
} }
} }

8
backend/src/Squidex/Areas/IdentityServer/Controllers/Error/ErrorController.cs

@ -18,20 +18,18 @@ namespace Squidex.Areas.IdentityServer.Controllers.Error
public sealed class ErrorController : IdentityServerController public sealed class ErrorController : IdentityServerController
{ {
private readonly IIdentityServerInteractionService interaction; private readonly IIdentityServerInteractionService interaction;
private readonly SignInManager<IdentityUser> signInManager;
public ErrorController(IIdentityServerInteractionService interaction, SignInManager<IdentityUser> signInManager) public ErrorController(IIdentityServerInteractionService interaction)
{ {
this.interaction = interaction; this.interaction = interaction;
this.signInManager = signInManager;
} }
[Route("error/")] [Route("error/")]
public async Task<IActionResult> Error(string? errorId = null) public async Task<IActionResult> Error(string? errorId = null)
{ {
await signInManager.SignOutAsync(); await SignInManager.SignOutAsync();
var vm = new ErrorViewModel(); var vm = new ErrorVM();
if (!string.IsNullOrWhiteSpace(errorId)) if (!string.IsNullOrWhiteSpace(errorId))
{ {

2
backend/src/Squidex/Areas/IdentityServer/Controllers/Error/ErrorViewModel.cs → backend/src/Squidex/Areas/IdentityServer/Controllers/Error/ErrorVM.cs

@ -9,7 +9,7 @@ using IdentityServer4.Models;
namespace Squidex.Areas.IdentityServer.Controllers.Error namespace Squidex.Areas.IdentityServer.Controllers.Error
{ {
public class ErrorViewModel public class ErrorVM
{ {
public ErrorMessage Error { get; set; } public ErrorMessage Error { get; set; }
} }

36
backend/src/Squidex/Areas/IdentityServer/Controllers/IdentityServerController.cs

@ -6,14 +6,26 @@
// ========================================================================== // ==========================================================================
using System; using System;
using IdentityServer4.Services;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Hosting;
namespace Squidex.Areas.IdentityServer.Controllers namespace Squidex.Areas.IdentityServer.Controllers
{ {
[Area("IdentityServer")] [Area("IdentityServer")]
public abstract class IdentityServerController : Controller public abstract class IdentityServerController : Controller
{ {
public SignInManager<IdentityUser> SignInManager
{
get
{
return HttpContext.RequestServices.GetRequiredService<SignInManager<IdentityUser>>();
}
}
public override void OnActionExecuting(ActionExecutingContext context) public override void OnActionExecuting(ActionExecutingContext context)
{ {
var request = context.HttpContext.Request; var request = context.HttpContext.Request;
@ -23,5 +35,29 @@ namespace Squidex.Areas.IdentityServer.Controllers
context.Result = new NotFoundResult(); context.Result = new NotFoundResult();
} }
} }
protected IActionResult RedirectToReturnUrl(string? returnUrl)
{
if (string.IsNullOrWhiteSpace(returnUrl))
{
return Redirect("~/../");
}
var urlGenerator = HttpContext.RequestServices.GetRequiredService<IUrlGenerator>();
if (urlGenerator.IsAllowedHost(returnUrl))
{
return Redirect(returnUrl);
}
var interactions = HttpContext.RequestServices.GetRequiredService<IIdentityServerInteractionService>();
if (interactions.IsValidReturnUrl(returnUrl))
{
return Redirect(returnUrl);
}
return Redirect("~/../");
}
} }
} }

31
backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs

@ -33,20 +33,17 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
public sealed class ProfileController : IdentityServerController public sealed class ProfileController : IdentityServerController
{ {
private static readonly ResizeOptions ResizeOptions = new ResizeOptions { Width = 128, Height = 128, Mode = ResizeMode.Crop }; private static readonly ResizeOptions ResizeOptions = new ResizeOptions { Width = 128, Height = 128, Mode = ResizeMode.Crop };
private readonly SignInManager<IdentityUser> signInManager;
private readonly IUserPictureStore userPictureStore; private readonly IUserPictureStore userPictureStore;
private readonly IUserService userService; private readonly IUserService userService;
private readonly IAssetThumbnailGenerator assetThumbnailGenerator; private readonly IAssetThumbnailGenerator assetThumbnailGenerator;
private readonly MyIdentityOptions identityOptions; private readonly MyIdentityOptions identityOptions;
public ProfileController( public ProfileController(
SignInManager<IdentityUser> signInManager, IOptions<MyIdentityOptions> identityOptions,
IUserPictureStore userPictureStore, IUserPictureStore userPictureStore,
IUserService userService, IUserService userService,
IAssetThumbnailGenerator assetThumbnailGenerator, IAssetThumbnailGenerator assetThumbnailGenerator)
IOptions<MyIdentityOptions> identityOptions)
{ {
this.signInManager = signInManager;
this.identityOptions = identityOptions.Value; this.identityOptions = identityOptions.Value;
this.userPictureStore = userPictureStore; this.userPictureStore = userPictureStore;
this.userService = userService; this.userService = userService;
@ -59,7 +56,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
{ {
var user = await userService.GetAsync(User); var user = await userService.GetAsync(User);
return View(await GetProfileVM<None>(user, successMessage: successMessage)); return View(await GetVM<None>(user, successMessage: successMessage));
} }
[HttpPost] [HttpPost]
@ -69,7 +66,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
var properties = var properties =
signInManager.ConfigureExternalAuthenticationProperties(provider, SignInManager.ConfigureExternalAuthenticationProperties(provider,
Url.Action(nameof(AddLoginCallback)), userService.GetUserId(User)); Url.Action(nameof(AddLoginCallback)), userService.GetUserId(User));
return Challenge(properties, provider); return Challenge(properties, provider);
@ -148,7 +145,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
private async Task AddLoginAsync(string id) private async Task AddLoginAsync(string id)
{ {
var externalLogin = await signInManager.GetExternalLoginInfoWithDisplayNameAsync(id); var externalLogin = await SignInManager.GetExternalLoginInfoWithDisplayNameAsync(id);
await userService.AddLoginAsync(id, externalLogin); await userService.AddLoginAsync(id, externalLogin);
} }
@ -192,7 +189,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
if (!ModelState.IsValid) if (!ModelState.IsValid)
{ {
return View(nameof(Profile), await GetProfileVM(user, model)); return View(nameof(Profile), await GetVM(user, model));
} }
string errorMessage; string errorMessage;
@ -200,7 +197,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
{ {
await action(user.Id); await action(user.Id);
await signInManager.SignInAsync((IdentityUser)user.Identity, true); await SignInManager.SignInAsync((IdentityUser)user.Identity, true);
return RedirectToAction(nameof(Profile), new { successMessage }); return RedirectToAction(nameof(Profile), new { successMessage });
} }
@ -213,10 +210,10 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
errorMessage = T.Get("users.errorHappened"); 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<ProfileVM> GetProfileVM<TModel>(IUser? user, TModel? model = null, string? errorMessage = null, string? successMessage = null) where TModel : class private async Task<ProfileVM> GetVM<TModel>(IUser? user, TModel? model = null, string? errorMessage = null, string? successMessage = null) where TModel : class
{ {
if (user == null) if (user == null)
{ {
@ -224,11 +221,11 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
} }
var (providers, hasPassword, logins) = await AsyncHelper.WhenAll( var (providers, hasPassword, logins) = await AsyncHelper.WhenAll(
signInManager.GetExternalProvidersAsync(), SignInManager.GetExternalProvidersAsync(),
userService.HasPasswordAsync(user), userService.HasPasswordAsync(user),
userService.GetLoginsAsync(user)); userService.GetLoginsAsync(user));
var result = new ProfileVM var vm = new ProfileVM
{ {
Id = user.Id, Id = user.Id,
ClientSecret = user.Claims.ClientSecret()!, ClientSecret = user.Claims.ClientSecret()!,
@ -245,12 +242,12 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
if (model != null) 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;
} }
} }
} }

23
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; }
}
}

115
backend/src/Squidex/Areas/IdentityServer/Controllers/Setup/SetupController.cs

@ -0,0 +1,115 @@
// ==========================================================================
// 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.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
{
[AllowAnonymous]
public class SetupController : IdentityServerController
{
private readonly IUrlGenerator urlGenerator;
private readonly IUserService userService;
private readonly MyIdentityOptions identityOptions;
public SetupController(IOptions<MyIdentityOptions> identityOptions,
IUrlGenerator urlGenerator,
IUserService userService)
{
this.urlGenerator = urlGenerator;
this.userService = userService;
this.identityOptions = identityOptions.Value;
}
[HttpGet]
[Route("setup/")]
public async Task<IActionResult> Setup()
{
if (!await userService.IsEmptyAsync())
{
return RedirectToReturnUrl(null);
}
return View(nameof(Setup), await GetVM(None.Value));
}
[HttpPost]
[Route("setup/")]
public async Task<IActionResult> 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<SetupVM> GetVM<TModel>(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,
IsValidHttps = HttpContext.Request.IsHttps,
HasExternalLogin = externalProviders.Any(),
HasPasswordAuth = identityOptions.AllowPasswordAuth,
};
if (model != null)
{
SimpleMapper.Map(model, result);
}
return result;
}
}
}

28
backend/src/Squidex/Areas/IdentityServer/Controllers/Setup/SetupVM.cs

@ -0,0 +1,28 @@
// ==========================================================================
// 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 HasExternalLogin { get; set; }
public bool HasPasswordAuth { get; set; }
public bool HasPasswordAndExternal { get; set; }
}
}

2
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"); ViewBag.Title = T.Get("users.accessDenied.title");
} }

2
backend/src/Squidex/Areas/IdentityServer/Views/Account/Consent.cshtml

@ -1,7 +1,7 @@
@model Squidex.Areas.IdentityServer.Controllers.Account.ConsentVM @model Squidex.Areas.IdentityServer.Controllers.Account.ConsentVM
@{ @{
ViewBag.Class = "profile-lg"; ViewBag.ThemeColor = "white";
ViewBag.Title = T.Get("users.consent.title"); ViewBag.Title = T.Get("users.consent.title");
} }

2
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"); ViewBag.Title = T.Get("users.lockedOutTitle");
} }

2
backend/src/Squidex/Areas/IdentityServer/Views/Account/Login.cshtml

@ -1,6 +1,8 @@
@model Squidex.Areas.IdentityServer.Controllers.Account.LoginVM @model Squidex.Areas.IdentityServer.Controllers.Account.LoginVM
@{ @{
ViewBag.ThemeColor = "white";
var action = Model.IsLogin ? T.Get("common.login") : T.Get("common.signup"); var action = Model.IsLogin ? T.Get("common.login") : T.Get("common.signup");
ViewBag.Title = action; ViewBag.Title = action;

2
backend/src/Squidex/Areas/IdentityServer/Views/Account/LogoutCompleted.cshtml

@ -1,4 +1,6 @@
@{ @{
ViewBag.ThemeColor = "white";
ViewBag.Title = T.Get("users.logout.title"); ViewBag.Title = T.Get("users.logout.title");
} }

4
backend/src/Squidex/Areas/IdentityServer/Views/Error/Error.cshtml

@ -1,7 +1,7 @@
@model Squidex.Areas.IdentityServer.Controllers.Error.ErrorViewModel @model Squidex.Areas.IdentityServer.Controllers.Error.ErrorVM
@{ @{
ViewBag.Theme = "white"; ViewBag.ThemeColor = "white";
ViewBag.Title = T.Get("users.error.title"); ViewBag.Title = T.Get("users.error.title");
} }

3
backend/src/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml

@ -1,7 +1,8 @@
@model Squidex.Areas.IdentityServer.Controllers.Profile.ProfileVM @model Squidex.Areas.IdentityServer.Controllers.Profile.ProfileVM
@{ @{
ViewBag.Class = "profile-lg"; ViewBag.ThemeColor = "white";
ViewBag.ThemeSize = "profile-lg";
ViewBag.Title = T.Get("users.profile.title"); ViewBag.Title = T.Get("users.profile.title");

167
backend/src/Squidex/Areas/IdentityServer/Views/Setup/Setup.cshtml

@ -0,0 +1,167 @@
@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)
{
<div class="errors-container">
<span class="errors">Html.ValidationMessage(field)</span>
</div>
}
}
}
<div class="card">
<div class="card-body">
<h1>@T.Get("setup.headline")</h1>
<img style="height: 250px" class="mt-2 mb-2" src="@Url.RootContentUrl("~/squid.svg?title=Welcome&text=Welcome%20to%20the%20Installation%20Process&face=happy")" />
<small class="form-text text-muted mt-2 mb-2">@T.Get("setup.hint")</small>
<div class="profile-section">
<h2>System Status</h2>
<div class="row mt-4">
@if (Model.IsValidHttps)
{
<div class="col-auto">
<div class="status-icon status-icon-success mt-1">
<i class="icon-checkmark"></i>
</div>
</div>
<div class="col">
@Html.Raw(T.Get("setup.https.success"))
</div>
}
else
{
<div class="col-auto">
<div class="status-icon status-icon-failed mt-1">
<i class="icon-exclamation"></i>
</div>
</div>
<div class="col">
@Html.Raw(T.Get("setup.https.failure"))
</div>
}
</div>
<div class="row mt-2">
@if (Model.BaseUrlConfigured == Model.BaseUrlCurrent)
{
<div class="col-auto">
<div class="status-icon status-icon-success mt-1">
<i class="icon-checkmark"></i>
</div>
</div>
<div class="col">
@Html.Raw(T.Get("setup.url.success"))
</div>
}
else
{
<div class="col-auto">
<div class="status-icon status-icon-failed mt-1">
<i class="icon-exclamation"></i>
</div>
</div>
<div class="col">
@Html.Raw(T.Get("setup.url.failure", new { actual = Model.BaseUrlCurrent, configured = Model.BaseUrlConfigured }))
</div>
}
</div>
</div>
<div class="profile-section">
<h2>@T.Get("setup.createUser.headline")</h2>
@if (Model.HasExternalLogin)
{
<div>
<small class="form-text text-muted mt-2 mb-2">@T.Get("setup.createUser.loginHint")</small>
<div class="mt-3">
<a class="btn btn-primary" asp-controller="Account" asp-action="Login">
@T.Get("setup.createUser.loginLink")
</a>
</div>
</div>
}
@if (Model.HasExternalLogin && Model.HasPasswordAuth)
{
<div class="profile-separator">
<div class="profile-separator-text">@T.Get("setup.createUser.separator")</div>
</div>
}
@if (Model.HasExternalLogin)
{
<h3>@T.Get("setup.createUser.headlineCreate")</h3>
@if (!string.IsNullOrWhiteSpace(Model.ErrorMessage))
{
<div class="form-alert form-alert-error">
@Model.ErrorMessage
</div>
}
<form class="profile-form" asp-controller="Setup" asp-action="Setup" method="post">
<div class="form-group">
<label for="email">@T.Get("common.email")</label>
@{ RenderValidation("Email"); }
<input type="text" class="form-control" name="email" id="email" />
</div>
<div class="form-group">
<label for="password">@T.Get("common.password")</label>
@{ RenderValidation("Password"); }
<input type="password" class="form-control" name="password" id="password" />
</div>
<div class="form-group">
<label for="passwordConfirm">@T.Get("setup.createUser.confirmPassword")</label>
@{ RenderValidation("PasswordConfirm"); }
<input type="password" class="form-control" name="passwordConfirm" id="passwordConfirm" />
</div>
<div class="form-group">
<button type="submit" class="btn btn-success">@T.Get("setup.createUser.button")</button>
</div>
</form>
}
@if (!Model.HasExternalLogin && !Model.HasPasswordAuth)
{
<div>
@T.Get("setup.createUser.failure")
</div>
}
</div>
</div>
</div>
<div class="text-center mt-4 mb-2">
<small class="text-muted">
@T.Get("setup.madeBy")<br />@T.Get("setup.madeByCopyright")
</small>
</div>

4
backend/src/Squidex/Areas/IdentityServer/Views/_Layout.cshtml

@ -17,8 +17,8 @@
@await RenderSectionAsync("header") @await RenderSectionAsync("header")
} }
</head> </head>
<body class="white"> <body class="@ViewBag.ThemeColor">
<div class="profile @ViewBag.Class"> <div class="profile @ViewBag.ThemeSize">
<img class="profile-logo" alt="@T.Get("common.product")S" title="@T.Get("common.product")" src="@Url.RootContentUrl("~/images/logo.svg")" /> <img class="profile-logo" alt="@T.Get("common.product")S" title="@T.Get("common.product")" src="@Url.RootContentUrl("~/images/logo.svg")" />
@RenderBody() @RenderBody()

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

@ -51,21 +51,9 @@ namespace Squidex.Config.Web
services.AddSingletonAs<SchemaResolver>() services.AddSingletonAs<SchemaResolver>()
.AsSelf(); .AsSelf();
services.AddSingletonAs<RobotsTxtMiddleware>()
.AsSelf();
services.AddSingletonAs<LocalCacheMiddleware>()
.AsSelf();
services.AddSingletonAs<UsageMiddleware>() services.AddSingletonAs<UsageMiddleware>()
.AsSelf(); .AsSelf();
services.AddSingletonAs<RequestExceptionMiddleware>()
.AsSelf();
services.AddSingletonAs<RequestLogPerformanceMiddleware>()
.AsSelf();
services.AddSingletonAs(c => translator) services.AddSingletonAs(c => translator)
.As<ILocalizer>(); .As<ILocalizer>();

19
backend/src/Squidex/Pipeline/Robots/RobotsTxtMiddleware.cs

@ -8,29 +8,28 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Squidex.Infrastructure;
namespace Squidex.Pipeline.Robots 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> robotsTxtOptions) public RobotsTxtMiddleware(RequestDelegate next)
{ {
Guard.NotNull(robotsTxtOptions, nameof(robotsTxtOptions)); this.next = next;
this.robotsTxtOptions = robotsTxtOptions.Value;
} }
public async Task InvokeAsync(HttpContext context, RequestDelegate next) public async Task InvokeAsync(HttpContext context, IOptions<RobotsTxtOptions> 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.ContentType = "text/plain";
context.Response.StatusCode = 200; context.Response.StatusCode = 200;
await context.Response.WriteAsync(robotsTxtOptions.Text); await context.Response.WriteAsync(text);
} }
else else
{ {

91
backend/src/Squidex/Pipeline/Squid/SquidMiddleware.cs

@ -15,7 +15,6 @@ namespace Squidex.Pipeline.Squid
{ {
public sealed class SquidMiddleware public sealed class SquidMiddleware
{ {
private readonly RequestDelegate next;
private readonly string squidHappyLG = LoadSvg("happy"); private readonly string squidHappyLG = LoadSvg("happy");
private readonly string squidHappySM = LoadSvg("happy-sm"); private readonly string squidHappySM = LoadSvg("happy-sm");
private readonly string squidSadLG = LoadSvg("sad"); private readonly string squidSadLG = LoadSvg("sad");
@ -23,76 +22,68 @@ namespace Squidex.Pipeline.Squid
public SquidMiddleware(RequestDelegate next) public SquidMiddleware(RequestDelegate next)
{ {
this.next = next;
} }
public async Task InvokeAsync(HttpContext context) public async Task InvokeAsync(HttpContext context)
{ {
var request = context.Request; var request = context.Request;
if (request.Path.Equals("/squid.svg")) var face = "sad";
{
var face = "sad";
if (request.Query.TryGetValue("face", out var faceValue) && (faceValue == "sad" || faceValue == "happy"))
{
face = faceValue;
}
var isSad = face == "sad"; if (request.Query.TryGetValue("face", out var faceValue) && (faceValue == "sad" || faceValue == "happy"))
{
var title = isSad ? "OH DAMN!" : "OH YEAH!"; face = faceValue;
}
if (request.Query.TryGetValue("title", out var titleValue) && !string.IsNullOrWhiteSpace(titleValue))
{
title = titleValue;
}
var text = "text"; var isSad = face == "sad";
if (request.Query.TryGetValue("text", out var textValue) && !string.IsNullOrWhiteSpace(textValue)) var title = isSad ? "OH DAMN!" : "OH YEAH!";
{
text = textValue;
}
var background = isSad ? "#F5F5F9" : "#4CC159"; if (request.Query.TryGetValue("title", out var titleValue) && !string.IsNullOrWhiteSpace(titleValue))
{
if (request.Query.TryGetValue("background", out var backgroundValue) && !string.IsNullOrWhiteSpace(backgroundValue)) title = titleValue;
{ }
background = backgroundValue;
}
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) var background = isSad ? "#F5F5F9" : "#4CC159";
{
svg = isSad ? squidSadSM : squidHappySM;
}
else
{
svg = isSad ? squidSadLG : squidHappyLG;
}
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()); var isSmall = request.Query.TryGetValue("small", out _);
svg = svg.Replace("{{TEXT1}}", l1);
svg = svg.Replace("{{TEXT2}}", l2);
svg = svg.Replace("{{TEXT3}}", l3);
svg = svg.Replace("[COLOR]", background);
context.Response.StatusCode = 200; string svg;
context.Response.ContentType = "image/svg+xml";
context.Response.Headers["Cache-Control"] = "public, max-age=604800";
await context.Response.WriteAsync(svg); if (isSmall)
{
svg = isSad ? squidSadSM : squidHappySM;
} }
else 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) private static (string, string, string) SplitText(string text)

8
frontend/app/framework/angular/status-icon.component.html

@ -1,14 +1,14 @@
<ng-container [ngSwitch]="status"> <ng-container [ngSwitch]="status">
<div *ngSwitchCase="'Failed'" class="status status-failed {{size}}" [title]="statusText"> <div *ngSwitchCase="'Failed'" class="status-icon status-icon-failed status-icon-{{size}}" [title]="statusText">
<i class="icon-exclamation"></i> <i class="icon-exclamation"></i>
</div> </div>
<div *ngSwitchCase="'Success'" class="status status-success {{size}}" [title]="statusText"> <div *ngSwitchCase="'Success'" class="status-icon status-icon-success status-icon-{{size}}" [title]="statusText">
<i class="icon-checkmark"></i> <i class="icon-checkmark"></i>
</div> </div>
<div *ngSwitchCase="'Completed'" class="status status-success {{size}}" [title]="statusText"> <div *ngSwitchCase="'Completed'" class="status-icon status-icon-success status-icon-{{size}}" [title]="statusText">
<i class="icon-checkmark"></i> <i class="icon-checkmark"></i>
</div> </div>
<div *ngSwitchDefault class="status status-pending spin {{size}}" [title]="statusText"> <div *ngSwitchDefault class="status status-icon-status-icon status-icon-{{size}} spin" [title]="statusText">
<i class="icon-hour-glass"></i> <i class="icon-hour-glass"></i>
</div> </div>
</ng-container> </ng-container>

28
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;
}
}

6
frontend/app/shell/pages/home/home-page.component.html

@ -17,7 +17,9 @@
<div class="login-arms"></div> <div class="login-arms"></div>
</div> </div>
<div class="proudly-made"> <div class="proudly-made text-center mt-4 mb-4">
{{ 'start.madeBy' | sqxTranslate }}<br> {{ 'start.madeByCopyright' | sqxTranslate }} <small class="text-muted">
{{ 'start.madeBy' | sqxTranslate }}<br> {{ 'start.madeByCopyright' | sqxTranslate }}
</small>
</div> </div>
</div> </div>

9
frontend/app/shell/pages/home/home-page.component.scss

@ -35,12 +35,3 @@
margin-top: 2rem; 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;
}

33
frontend/app/theme/_common.scss

@ -215,6 +215,39 @@ 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;
}
&-failed {
background: $color-theme-error;
}
&-success {
background: $color-theme-green;
}
}
// //
// Animations // Animations
// //

Loading…
Cancel
Save