mirror of https://github.com/Squidex/squidex.git
38 changed files with 751 additions and 222 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,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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; } |
||||
|
} |
||||
|
} |
||||
@ -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> |
||||
@ -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> |
||||
@ -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