mirror of https://github.com/Squidex/squidex.git
Browse Source
* Setup guide. * Better content separation. * Minor style fixes. * Build fix.pull/627/head
committed by
GitHub
41 changed files with 939 additions and 262 deletions
@ -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); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
} |
|||
@ -0,0 +1,126 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Authorization; |
|||
using Microsoft.AspNetCore.Identity; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Microsoft.Extensions.Options; |
|||
using Squidex.Areas.Api.Controllers.UI; |
|||
using Squidex.Assets; |
|||
using Squidex.Config; |
|||
using Squidex.Domain.Users; |
|||
using Squidex.Hosting; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Reflection; |
|||
using Squidex.Infrastructure.Translations; |
|||
using Squidex.Infrastructure.Validation; |
|||
|
|||
namespace Squidex.Areas.IdentityServer.Controllers.Setup |
|||
{ |
|||
public class SetupController : IdentityServerController |
|||
{ |
|||
private readonly IAssetStore assetStore; |
|||
private readonly IUrlGenerator urlGenerator; |
|||
private readonly IUserService userService; |
|||
private readonly MyUIOptions uiOptions; |
|||
private readonly MyIdentityOptions identityOptions; |
|||
|
|||
public SetupController( |
|||
IAssetStore assetStore, |
|||
IOptions<MyUIOptions> uiOptions, |
|||
IOptions<MyIdentityOptions> identityOptions, |
|||
IUrlGenerator urlGenerator, |
|||
IUserService userService) |
|||
{ |
|||
this.assetStore = assetStore; |
|||
this.identityOptions = identityOptions.Value; |
|||
this.uiOptions = uiOptions.Value; |
|||
this.urlGenerator = urlGenerator; |
|||
this.userService = userService; |
|||
} |
|||
|
|||
[HttpGet] |
|||
[Route("setup/")] |
|||
public async Task<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, |
|||
EverybodyCanCreateApps = !uiOptions.OnlyAdminsCanCreateApps, |
|||
IsValidHttps = HttpContext.Request.IsHttps, |
|||
IsAssetStoreFile = assetStore is FolderAssetStore, |
|||
IsAssetStoreFtp = assetStore is FTPAssetStore, |
|||
HasExternalLogin = externalProviders.Any(), |
|||
HasPasswordAuth = identityOptions.AllowPasswordAuth, |
|||
}; |
|||
|
|||
if (model != null) |
|||
{ |
|||
SimpleMapper.Map(model, result); |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
namespace Squidex.Areas.IdentityServer.Controllers.Setup |
|||
{ |
|||
public sealed class SetupVM |
|||
{ |
|||
public string Email { get; set; } |
|||
|
|||
public string BaseUrlCurrent { get; set; } |
|||
|
|||
public string BaseUrlConfigured { get; set; } |
|||
|
|||
public string? ErrorMessage { get; set; } |
|||
|
|||
public bool IsValidHttps { get; set; } |
|||
|
|||
public bool IsAssetStoreFtp { get; set; } |
|||
|
|||
public bool IsAssetStoreFile { get; set; } |
|||
|
|||
public bool EverybodyCanCreateApps { get; set; } |
|||
|
|||
public bool HasExternalLogin { get; set; } |
|||
|
|||
public bool HasPasswordAuth { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,201 @@ |
|||
@model Squidex.Areas.IdentityServer.Controllers.Setup.SetupVM |
|||
|
|||
@{ |
|||
ViewBag.ThemeColor = "gray"; |
|||
ViewBag.ThemeSize = "profile-lg"; |
|||
|
|||
ViewBag.Title = T.Get("setup.title"); |
|||
|
|||
void RenderValidation(string field) |
|||
{ |
|||
@if (ViewContext.ViewData.ModelState[field]?.ValidationState == Microsoft.AspNetCore.Mvc.ModelBinding.ModelValidationState.Invalid) |
|||
{ |
|||
<div class="errors-container"> |
|||
<span class="errors">Html.ValidationMessage(field)</span> |
|||
</div> |
|||
} |
|||
} |
|||
|
|||
void RenderRuleAsSuccess(string message) |
|||
{ |
|||
<div class="row mt-4"> |
|||
<div class="col-auto"> |
|||
<div class="status-icon status-icon-success mt-2"> |
|||
<i class="icon-checkmark"></i> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="col"> |
|||
<div> |
|||
@Html.Raw(message) |
|||
</div> |
|||
</div> |
|||
</div> |
|||
} |
|||
|
|||
void RenderRuleAsCritical(string message) |
|||
{ |
|||
<div class="row mt-4"> |
|||
<div class="col-auto"> |
|||
<div class="status-icon status-icon-failed mt-2"> |
|||
<i class="icon-exclamation"></i> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="col"> |
|||
<div> |
|||
<strong>@T.Get("common.critical")</strong>: @Html.Raw(message) |
|||
</div> |
|||
</div> |
|||
</div> |
|||
} |
|||
|
|||
void RenderRuleAsWarning(string message) |
|||
{ |
|||
<div class="row mt-4"> |
|||
<div class="col-auto"> |
|||
<div class="status-icon status-icon-warning mt-2"> |
|||
<i class="icon-exclamation"></i> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="col"> |
|||
<div> |
|||
<strong>@T.Get("common.warning")</strong>: @Html.Raw(message) |
|||
</div> |
|||
</div> |
|||
</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>@T.Get("setup.rules.headline")</h2> |
|||
|
|||
@if (Model.IsValidHttps) |
|||
{ |
|||
RenderRuleAsSuccess(T.Get("setup.ruleHttps.success")); |
|||
} |
|||
else |
|||
{ |
|||
RenderRuleAsCritical(T.Get("setup.ruleHttps.failure")); |
|||
} |
|||
|
|||
@if (Model.BaseUrlConfigured == Model.BaseUrlCurrent) |
|||
{ |
|||
RenderRuleAsSuccess(T.Get("setup.ruleUrl.success")); |
|||
} |
|||
else |
|||
{ |
|||
RenderRuleAsCritical(T.Get("setup.ruleUrl.failure", new { actual = Model.BaseUrlCurrent, configured = Model.BaseUrlConfigured })); |
|||
} |
|||
|
|||
@if (Model.EverybodyCanCreateApps) |
|||
{ |
|||
RenderRuleAsWarning(T.Get("setup.ruleAppCreation.warningAdmins")); |
|||
} |
|||
else |
|||
{ |
|||
RenderRuleAsWarning(T.Get("setup.ruleAppCreation.warningAll")); |
|||
} |
|||
|
|||
@if (Model.IsAssetStoreFtp) |
|||
{ |
|||
RenderRuleAsWarning(T.Get("setup.ruleFtp.warning")); |
|||
} |
|||
|
|||
@if (Model.IsAssetStoreFile) |
|||
{ |
|||
RenderRuleAsWarning(T.Get("setup.ruleFolder.warning")); |
|||
} |
|||
</div> |
|||
|
|||
<hr /> |
|||
|
|||
<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.HasPasswordAuth) |
|||
{ |
|||
<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> |
|||
@ -1,14 +1,14 @@ |
|||
<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> |
|||
</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> |
|||
</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> |
|||
</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> |
|||
</div> |
|||
</ng-container> |
|||
@ -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; |
|||
} |
|||
} |
|||
Loading…
Reference in new issue