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..7af66a2d6 100644 --- a/backend/i18n/source/backend_en.json +++ b/backend/i18n/source/backend_en.json @@ -269,6 +269,23 @@ "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": "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 X-Forwarded-* 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 URLS__BASEURL environment variable. The current base URL {actual} does not match to the base url {configured}.", + "setup.url.success": "Congratulations the URLS__BASEURL environment variable is configured properly.", "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..514e66d05 100644 --- a/backend/src/Squidex.Shared/Texts.it.resx +++ b/backend/src/Squidex.Shared/Texts.it.resx @@ -892,6 +892,57 @@ 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 + + + One Time 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. + + + 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). + + + Proudly made by + + + Sebastian Stehle and Contributors, 2016-2021 + + + Installation + + + 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. + 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..d5d629380 100644 --- a/backend/src/Squidex.Shared/Texts.nl.resx +++ b/backend/src/Squidex.Shared/Texts.nl.resx @@ -892,6 +892,57 @@ 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 + + + One Time 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. + + + 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). + + + Proudly made by + + + Sebastian Stehle and Contributors, 2016-2021 + + + Installation + + + 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. + 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..1d19f4c1d 100644 --- a/backend/src/Squidex.Shared/Texts.resx +++ b/backend/src/Squidex.Shared/Texts.resx @@ -892,6 +892,57 @@ 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 + + + One Time 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. + + + 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). + + + Proudly made by + + + Sebastian Stehle and Contributors, 2016-2021 + + + Installation + + + 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. + 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..da5ff8560 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,8 +227,8 @@ 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 +242,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 +252,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 +346,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 +362,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/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..83d5b9e62 --- /dev/null +++ b/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 identityOptions, + IUrlGenerator urlGenerator, + IUserService userService) + { + this.urlGenerator = urlGenerator; + this.userService = userService; + this.identityOptions = identityOptions.Value; + } + + [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, + IsValidHttps = HttpContext.Request.IsHttps, + 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..a7f4ad869 --- /dev/null +++ b/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; } + } +} 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..b294704ac 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Views/Account/Consent.cshtml +++ b/backend/src/Squidex/Areas/IdentityServer/Views/Account/Consent.cshtml @@ -1,7 +1,7 @@ @model Squidex.Areas.IdentityServer.Controllers.Account.ConsentVM @{ - ViewBag.Class = "profile-lg"; + ViewBag.ThemeColor = "white"; 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..e6d5f4a18 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Views/Account/Login.cshtml +++ b/backend/src/Squidex/Areas/IdentityServer/Views/Account/Login.cshtml @@ -1,6 +1,8 @@ @model Squidex.Areas.IdentityServer.Controllers.Account.LoginVM @{ + ViewBag.ThemeColor = "white"; + var action = Model.IsLogin ? T.Get("common.login") : T.Get("common.signup"); ViewBag.Title = action; diff --git a/backend/src/Squidex/Areas/IdentityServer/Views/Account/LogoutCompleted.cshtml b/backend/src/Squidex/Areas/IdentityServer/Views/Account/LogoutCompleted.cshtml index f7f882186..07082b292 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Views/Account/LogoutCompleted.cshtml +++ b/backend/src/Squidex/Areas/IdentityServer/Views/Account/LogoutCompleted.cshtml @@ -1,4 +1,6 @@ @{ + ViewBag.ThemeColor = "white"; + ViewBag.Title = T.Get("users.logout.title"); } diff --git a/backend/src/Squidex/Areas/IdentityServer/Views/Error/Error.cshtml b/backend/src/Squidex/Areas/IdentityServer/Views/Error/Error.cshtml index 5775b1ec8..849aa36d8 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Views/Error/Error.cshtml +++ b/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"); } diff --git a/backend/src/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml b/backend/src/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml index fbbf5604d..103e3ad5d 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml +++ b/backend/src/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml @@ -1,7 +1,8 @@ @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"); 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..802ea69e5 --- /dev/null +++ b/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) + { +
+ Html.ValidationMessage(field) +
+ } + } +} + +
+
+

@T.Get("setup.headline")

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

System Status

+ +
+ @if (Model.IsValidHttps) + { +
+
+ +
+
+ +
+ @Html.Raw(T.Get("setup.https.success")) +
+ } + else + { +
+
+ +
+ +
+ +
+ @Html.Raw(T.Get("setup.https.failure")) +
+ } +
+ +
+ @if (Model.BaseUrlConfigured == Model.BaseUrlCurrent) + { +
+
+ +
+
+ +
+ @Html.Raw(T.Get("setup.url.success")) +
+ } + else + { +
+
+ +
+ +
+ +
+ @Html.Raw(T.Get("setup.url.failure", new { actual = Model.BaseUrlCurrent, configured = Model.BaseUrlConfigured })) +
+ } +
+
+ +
+

@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.HasExternalLogin) + { +

@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") +
+
\ No newline at end of file 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/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..a9048fc7b 100644 --- a/frontend/app/theme/_common.scss +++ b/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 //