Browse Source

Merge branch 'master' of github.com:Squidex/squidex

pull/511/head
Sebastian 6 years ago
parent
commit
3856168666
  1. 48
      backend/src/Squidex.Domain.Users/UserManagerExtensions.cs
  2. 104
      backend/src/Squidex.Domain.Users/UserValues.cs
  3. 2
      backend/src/Squidex.Shared/Identity/SquidexClaimTypes.cs
  4. 10
      backend/src/Squidex.Shared/Users/UserExtensions.cs
  5. 24
      backend/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs
  6. 1
      backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ChangeProfileModel.cs
  7. 25
      backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ChangePropertiesModel.cs
  8. 53
      backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs
  9. 2
      backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileVM.cs
  10. 30
      backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/UserProperty.cs
  11. 183
      backend/src/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml
  12. 1
      backend/src/Squidex/Config/Web/WebServices.cs
  13. 1
      backend/src/Squidex/Squidex.csproj

48
backend/src/Squidex.Domain.Users/UserManagerExtensions.cs

@ -133,13 +133,7 @@ namespace Squidex.Domain.Users
try
{
await DoChecked(() => userManager.CreateAsync(user), "Cannot create user.");
var claims = values.ToClaims(true);
if (claims.Count > 0)
{
await DoChecked(() => userManager.AddClaimsAsync(user, claims), "Cannot add user.");
}
await DoChecked(() => values.SyncClaims(userManager, user), "Cannot add user.");
if (!string.IsNullOrWhiteSpace(values.Password))
{
@ -172,9 +166,12 @@ namespace Squidex.Domain.Users
public static Task<IdentityResult> GenerateClientSecretAsync(this UserManager<IdentityUser> userManager, IdentityUser user)
{
var claims = new List<Claim> { new Claim(SquidexClaimTypes.ClientSecret, RandomHash.New()) };
var update = new UserValues
{
ClientSecret = RandomHash.New()
};
return userManager.SyncClaimsAsync(user, claims);
return update.SyncClaims(userManager, user);
}
public static async Task<IdentityResult> UpdateSafeAsync(this UserManager<IdentityUser> userManager, IdentityUser user, UserValues values)
@ -204,7 +201,7 @@ namespace Squidex.Domain.Users
await DoChecked(() => userManager.SetUserNameAsync(user, values.Email), "Cannot update email.");
}
await DoChecked(() => userManager.SyncClaimsAsync(user, values.ToClaims(false)), "Cannot update user.");
await DoChecked(() => values.SyncClaims(userManager, user), "Cannot update user.");
if (!string.IsNullOrWhiteSpace(values.Password))
{
@ -251,36 +248,9 @@ namespace Squidex.Domain.Users
}
}
public static async Task<IdentityResult> SyncClaimsAsync(this UserManager<IdentityUser> userManager, IdentityUser user, List<Claim> claims)
public static Task<IdentityResult> SyncClaims(this UserManager<IdentityUser> userManager, IdentityUser user, UserValues values)
{
if (claims.Any())
{
var oldClaims = await userManager.GetClaimsAsync(user);
var oldClaimsToRemove = new List<Claim>();
foreach (var oldClaim in oldClaims)
{
if (claims.Any(x => x.Type == oldClaim.Type))
{
oldClaimsToRemove.Add(oldClaim);
}
}
if (oldClaimsToRemove.Count > 0)
{
var result = await userManager.RemoveClaimsAsync(user, oldClaimsToRemove);
if (!result.Succeeded)
{
return result;
}
}
return await userManager.AddClaimsAsync(user, claims.Where(x => !string.IsNullOrWhiteSpace(x.Value)));
}
return IdentityResult.Success;
return values.SyncClaims(userManager, user);
}
}
}

104
backend/src/Squidex.Domain.Users/UserValues.cs

@ -5,9 +5,12 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Squidex.Infrastructure.Security;
using Squidex.Shared.Identity;
@ -21,67 +24,120 @@ namespace Squidex.Domain.Users
public string? Password { get; set; }
public string? ClientSecret { get; set; }
public string Email { get; set; }
public bool? Hidden { get; set; }
public bool? Invited { get; set; }
public bool? Consent { get; set; }
public bool? ConsentForEmails { get; set; }
public bool? Hidden { get; set; }
public PermissionSet? Permissions { get; set; }
public PermissionSet Permissions { get; set; }
public List<Claim>? CustomClaims { get; set; }
public List<Claim> ToClaims(bool initial)
{
return ToClaimsCore(initial).ToList();
}
public List<(string Name, string Value)>? Properties { get; set; }
private IEnumerable<Claim> ToClaimsCore(bool initial)
internal async Task<IdentityResult> SyncClaims(UserManager<IdentityUser> userManager, IdentityUser user)
{
if (!string.IsNullOrWhiteSpace(DisplayName))
var current = await userManager.GetClaimsAsync(user);
var claimsToRemove = new List<Claim>();
var claimsToAdd = new List<Claim>();
void RemoveClaims(Func<Claim, bool> predicate)
{
yield return new Claim(SquidexClaimTypes.DisplayName, DisplayName);
claimsToRemove.AddRange(current.Where(predicate));
}
if (!string.IsNullOrWhiteSpace(PictureUrl))
void AddClaim(string type, string value)
{
yield return new Claim(SquidexClaimTypes.PictureUrl, PictureUrl);
claimsToAdd.Add(new Claim(type, value));
}
if (Hidden.HasValue)
void SyncString(string type, string? value)
{
yield return new Claim(SquidexClaimTypes.Hidden, Hidden.ToString());
if (value != null)
{
RemoveClaims(x => x.Type == type);
if (!string.IsNullOrWhiteSpace(value))
{
AddClaim(type, value);
}
}
}
if (Invited.HasValue)
void SyncBoolean(string type, bool? value)
{
yield return new Claim(SquidexClaimTypes.Invited, Invited.ToString());
if (value != null)
{
RemoveClaims(x => x.Type == type);
if (value == true)
{
AddClaim(type, value.ToString()!);
}
}
}
if (Consent.HasValue)
SyncString(SquidexClaimTypes.ClientSecret, ClientSecret);
SyncString(SquidexClaimTypes.DisplayName, DisplayName);
SyncString(SquidexClaimTypes.PictureUrl, PictureUrl);
SyncBoolean(SquidexClaimTypes.Hidden, Hidden);
SyncBoolean(SquidexClaimTypes.Invited, Invited);
SyncBoolean(SquidexClaimTypes.Consent, Consent);
SyncBoolean(SquidexClaimTypes.ConsentForEmails, ConsentForEmails);
if (CustomClaims != null)
{
yield return new Claim(SquidexClaimTypes.Consent, Consent.ToString());
foreach (var claim in CustomClaims)
{
SyncString(claim.Type, claim.Value);
}
}
if (ConsentForEmails.HasValue)
if (Permissions != null)
{
yield return new Claim(SquidexClaimTypes.ConsentForEmails, ConsentForEmails.ToString());
RemoveClaims(x => x.Type == SquidexClaimTypes.Permissions);
foreach (var permission in Permissions)
{
AddClaim(SquidexClaimTypes.Permissions, permission.Id);
}
}
if (Permissions != null)
if (Properties != null)
{
if (!initial)
RemoveClaims(x => x.Type.StartsWith(SquidexClaimTypes.CustomPrefix, StringComparison.OrdinalIgnoreCase));
foreach (var (name, value) in Properties)
{
yield return new Claim(SquidexClaimTypes.Permissions, string.Empty);
AddClaim($"{SquidexClaimTypes.CustomPrefix}:{name}", value);
}
}
foreach (var permission in Permissions)
if (claimsToRemove.Count > 0)
{
var result = await userManager.RemoveClaimsAsync(user, claimsToRemove);
if (!result.Succeeded)
{
yield return new Claim(SquidexClaimTypes.Permissions, permission.Id);
return result;
}
}
if (claimsToAdd.Count > 0)
{
return await userManager.AddClaimsAsync(user, claimsToAdd);
}
return IdentityResult.Success;
}
}
}

2
backend/src/Squidex.Shared/Identity/SquidexClaimTypes.cs

@ -21,6 +21,8 @@ namespace Squidex.Shared.Identity
public static readonly string Invited = "urn:squidex:invited";
public static readonly string CustomPrefix = "urn:squidex:custom";
public static readonly string Permissions = "urn:squidex:permissions";
public static readonly string PermissionsClient = "client_urn:squidex:permissions";

10
backend/src/Squidex.Shared/Users/UserExtensions.cs

@ -6,6 +6,7 @@
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using Squidex.Infrastructure.Security;
using Squidex.Shared.Identity;
@ -76,7 +77,14 @@ namespace Squidex.Shared.Users
public static string[] GetClaimValues(this IUser user, string type)
{
return user.Claims.Where(x => string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase)).Select(x => x.Value).ToArray();
return user.Claims.Where(x => string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase))
.Select(x => x.Value).ToArray();
}
public static List<(string Name, string Value)> GetCustomProperties(this IUser user)
{
return user.Claims.Where(x => x.Type.StartsWith(SquidexClaimTypes.CustomPrefix, StringComparison.OrdinalIgnoreCase))
.Select(x => (x.Type.Substring(SquidexClaimTypes.CustomPrefix.Length + 1), x.Value)).ToList();
}
public static bool HasClaim(this IUser user, string type)

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

@ -24,6 +24,7 @@ using Squidex.Config;
using Squidex.Domain.Users;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Security;
using Squidex.Shared;
using Squidex.Shared.Identity;
using Squidex.Shared.Users;
@ -361,36 +362,27 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account
private Task<bool> AddClaimsAsync(UserWithClaims user, ExternalLoginInfo externalLogin, string email, bool isFirst = false)
{
var newClaims = new List<Claim>();
void AddClaim(Claim claim)
{
newClaims.Add(claim);
user.Claims.Add(claim);
}
foreach (var squidexClaim in externalLogin.Principal.GetSquidexClaims())
var update = new UserValues
{
AddClaim(squidexClaim);
}
CustomClaims = externalLogin.Principal.GetSquidexClaims().ToList()
};
if (!user.HasPictureUrl())
{
AddClaim(new Claim(SquidexClaimTypes.PictureUrl, GravatarHelper.CreatePictureUrl(email)));
update.PictureUrl = GravatarHelper.CreatePictureUrl(email);
}
if (!user.HasDisplayName())
{
AddClaim(new Claim(SquidexClaimTypes.DisplayName, email));
update.DisplayName = email;
}
if (isFirst)
{
AddClaim(new Claim(SquidexClaimTypes.Permissions, Permissions.Admin));
update.Permissions = new PermissionSet(Permissions.Admin);
}
return MakeIdentityOperation(() => userManager.SyncClaimsAsync(user.Identity, newClaims));
return MakeIdentityOperation(() => userManager.SyncClaims(user.Identity, update));
}
private IActionResult RedirectToLogoutUrl(LogoutRequest context)

1
backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ChangeProfileModel.cs

@ -13,7 +13,6 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
public class ChangeProfileModel
{
[Required(ErrorMessage = "Email is required.")]
[EmailAddress(ErrorMessage = "Email is not valid.")]
public string Email { get; set; }
[Required(ErrorMessage = "DisplayName is required.")]

25
backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ChangePropertiesModel.cs

@ -0,0 +1,25 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using Squidex.Domain.Users;
namespace Squidex.Areas.IdentityServer.Controllers.Profile
{
public class ChangePropertiesModel
{
public List<UserProperty> Properties { get; set; }
public UserValues ToValues()
{
var properties = Properties?.Select(x => x.ToTuple()).ToList() ?? new List<(string Name, string Value)>();
return new UserValues { Properties = properties };
}
}
}

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

@ -56,7 +56,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
{
var user = await userManager.GetUserWithClaimsAsync(User);
return View(await GetProfileVM(user, successMessage: successMessage));
return View(await GetProfileVM<None>(user, successMessage: successMessage));
}
[HttpPost]
@ -76,7 +76,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
[Route("/account/profile/login-add-callback/")]
public Task<IActionResult> AddLoginCallback()
{
return MakeChangeAsync(user => AddLoginAsync(user),
return MakeChangeAsync<None>(u => AddLoginAsync(u),
"Login added successfully.");
}
@ -84,39 +84,47 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
[Route("/account/profile/update/")]
public Task<IActionResult> UpdateProfile(ChangeProfileModel model)
{
return MakeChangeAsync(user => userManager.UpdateSafeAsync(user.Identity, model.ToValues()),
"Account updated successfully.");
return MakeChangeAsync(u => userManager.UpdateSafeAsync(u, model.ToValues()),
"Account updated successfully.", model);
}
[HttpPost]
[Route("/account/profile/properties/")]
public Task<IActionResult> UpdateProperties(ChangePropertiesModel model)
{
return MakeChangeAsync(u => userManager.UpdateSafeAsync(u, model.ToValues()),
"Account updated successfully.", model);
}
[HttpPost]
[Route("/account/profile/login-remove/")]
public Task<IActionResult> RemoveLogin(RemoveLoginModel model)
{
return MakeChangeAsync(user => userManager.RemoveLoginAsync(user.Identity, model.LoginProvider, model.ProviderKey),
"Login provider removed successfully.");
return MakeChangeAsync(u => userManager.RemoveLoginAsync(u, model.LoginProvider, model.ProviderKey),
"Login provider removed successfully.", model);
}
[HttpPost]
[Route("/account/profile/password-set/")]
public Task<IActionResult> SetPassword(SetPasswordModel model)
{
return MakeChangeAsync(user => userManager.AddPasswordAsync(user.Identity, model.Password),
"Password set successfully.");
return MakeChangeAsync(u => userManager.AddPasswordAsync(u, model.Password),
"Password set successfully.", model);
}
[HttpPost]
[Route("/account/profile/password-change/")]
public Task<IActionResult> ChangePassword(ChangePasswordModel model)
{
return MakeChangeAsync(user => userManager.ChangePasswordAsync(user.Identity, model.OldPassword, model.Password),
"Password changed successfully.");
return MakeChangeAsync(u => userManager.ChangePasswordAsync(u, model.OldPassword, model.Password),
"Password changed successfully.", model);
}
[HttpPost]
[Route("/account/profile/generate-client-secret/")]
public Task<IActionResult> GenerateClientSecret()
{
return MakeChangeAsync(user => userManager.GenerateClientSecretAsync(user.Identity),
return MakeChangeAsync<None>(user => userManager.GenerateClientSecretAsync(user),
"Client secret generated successfully.");
}
@ -124,18 +132,18 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
[Route("/account/profile/upload-picture/")]
public Task<IActionResult> UploadPicture(List<IFormFile> file)
{
return MakeChangeAsync(user => UpdatePictureAsync(file, user),
return MakeChangeAsync<None>(user => UpdatePictureAsync(file, user),
"Picture uploaded successfully.");
}
private async Task<IdentityResult> AddLoginAsync(UserWithClaims user)
private async Task<IdentityResult> AddLoginAsync(IdentityUser user)
{
var externalLogin = await signInManager.GetExternalLoginInfoWithDisplayNameAsync(userManager.GetUserId(User));
return await userManager.AddLoginAsync(user.Identity, externalLogin);
return await userManager.AddLoginAsync(user, externalLogin);
}
private async Task<IdentityResult> UpdatePictureAsync(List<IFormFile> file, UserWithClaims user)
private async Task<IdentityResult> UpdatePictureAsync(List<IFormFile> file, IdentityUser user)
{
if (file.Count != 1)
{
@ -158,10 +166,10 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
await userPictureStore.UploadAsync(user.Id, thumbnailStream);
}
return await userManager.UpdateSafeAsync(user.Identity, new UserValues { PictureUrl = SquidexClaimTypes.PictureUrlStore });
return await userManager.UpdateSafeAsync(user, new UserValues { PictureUrl = SquidexClaimTypes.PictureUrlStore });
}
private async Task<IActionResult> MakeChangeAsync(Func<UserWithClaims, Task<IdentityResult>> action, string successMessage, ChangeProfileModel? model = null)
private async Task<IActionResult> MakeChangeAsync<T>(Func<IdentityUser, Task<IdentityResult>> action, string successMessage, T? model = null) where T : class
{
var user = await userManager.GetUserWithClaimsAsync(User);
@ -178,7 +186,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
string errorMessage;
try
{
var result = await action(user);
var result = await action(user.Identity);
if (result.Succeeded)
{
@ -197,7 +205,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
return View(nameof(Profile), await GetProfileVM(user, model, errorMessage));
}
private async Task<ProfileVM> GetProfileVM(UserWithClaims? user, ChangeProfileModel? model = null, string? errorMessage = null, string? successMessage = null)
private async Task<ProfileVM> GetProfileVM<T>(UserWithClaims? user, T? model = null, string? errorMessage = null, string? successMessage = null) where T : class
{
if (user == null)
{
@ -219,9 +227,9 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
ExternalLogins = taskForLogins.Result,
ExternalProviders = taskForProviders.Result,
DisplayName = user.DisplayName()!,
IsHidden = user.IsHidden(),
HasPassword = taskForPassword.Result,
HasPasswordAuth = identityOptions.AllowPasswordAuth,
IsHidden = user.IsHidden(),
SuccessMessage = successMessage
};
@ -230,6 +238,11 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
SimpleMapper.Map(model, result);
}
if (result.Properties == null)
{
result.Properties = user.GetCustomProperties().Select(UserProperty.FromTuple).ToList();
}
return result;
}
}

2
backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileVM.cs

@ -30,6 +30,8 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
public bool HasPasswordAuth { get; set; }
public List<UserProperty> Properties { get; set; }
public IList<UserLoginInfo> ExternalLogins { get; set; }
public IList<ExternalProvider> ExternalProviders { get; set; }

30
backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/UserProperty.cs

@ -0,0 +1,30 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.ComponentModel.DataAnnotations;
namespace Squidex.Areas.IdentityServer.Controllers.Profile
{
public sealed class UserProperty
{
[Required(ErrorMessage = "Name is required.")]
public string Name { get; set; }
[Required(ErrorMessage = "Value is required.")]
public string Value { get; set; }
public (string Name, string Value) ToTuple()
{
return (Name, Value);
}
public static UserProperty FromTuple((string Name, string Value) value)
{
return new UserProperty { Name = value.Name, Value = value.Value };
}
}
}

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

@ -4,8 +4,18 @@
ViewBag.Class = "profile-lg";
ViewBag.Title = "Profile";
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>
}
}
}
<h1>Edit Profile</h1>
<h2>Personal Information</h2>
@ -23,7 +33,7 @@
@Model.ErrorMessage
</div>
}
<div class="row profile-section">
<div class="col profile-picture-col">
<img class="profile-picture" src="@Url.RootContentUrl($"~/api/users/{Model.Id}/picture/?q={Guid.NewGuid()}")" />
@ -42,41 +52,31 @@
<form class="profile-form profile-section" asp-controller="Profile" asp-action="UpdateProfile" method="post">
<div class="form-group">
<label for="email">Email</label>
@if (ViewContext.ViewData.ModelState["Email"]?.ValidationState == Microsoft.AspNetCore.Mvc.ModelBinding.ModelValidationState.Invalid)
{
<div class="errors-container">
<span asp-validation-for="Email" class="errors"></span>
</div>
}
<input type="email" ap class="form-control" asp-for="Email" id="email" />
@{ RenderValidation("Email"); }
<input type="email" class="form-control" asp-for="Email" />
</div>
<div class="form-group">
<label for="displayName">Display Name</label>
@if (ViewContext.ViewData.ModelState["DisplayName"]?.ValidationState == Microsoft.AspNetCore.Mvc.ModelBinding.ModelValidationState.Invalid)
{
<div class="errors-container">
<span asp-validation-for="DisplayName" class="errors"></span>
</div>
}
@{ RenderValidation("DisplayName"); }
<input type="text" class="form-control" asp-for="DisplayName" id="displayName"/>
<input type="text" class="form-control" asp-for="DisplayName" />
</div>
<div class="form-group">
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="IsHidden" id="hidden" />
<input type="checkbox" class="form-check-input" asp-for="IsHidden" />
<label class="form-check-label" for="hidden">Do not show my profile to other users</label>
<label class="form-check-label" asp-for="IsHidden">Do not show my profile to other users</label>
</div>
</div>
<button type="submit" class="btn btn-primary">Save</button>
</form>
@if (Model.ExternalProviders.Any())
{
<div class="profile-section">
@ -84,9 +84,9 @@
<table class="table table-fixed table-lesspadding">
<colgroup>
<col style="width: 100px;"/>
<col style="width: 100%;"/>
<col style="width: 100px;"/>
<col style="width: 100px;" />
<col style="width: 100%;" />
<col style="width: 100px;" />
</colgroup>
@foreach (var login in Model.ExternalLogins)
{
@ -101,8 +101,8 @@
@if (Model.ExternalLogins.Count > 1 || Model.HasPassword)
{
<form asp-controller="Profile" asp-action="RemoveLogin" method="post">
<input type="hidden" value="@login.LoginProvider" name="LoginProvider"/>
<input type="hidden" value="@login.ProviderKey" name="ProviderKey"/>
<input type="hidden" value="@login.LoginProvider" name="LoginProvider" />
<input type="hidden" value="@login.ProviderKey" name="ProviderKey" />
<button type="submit" class="btn btn-text-danger btn-sm">
Remove
@ -126,7 +126,7 @@
</form>
</div>
}
@if (Model.HasPasswordAuth)
{
<div class="profile-section">
@ -138,12 +138,7 @@
<div class="form-group">
<label for="oldPassword">Old Password</label>
@if (ViewContext.ViewData.ModelState["OldPassword"]?.ValidationState == Microsoft.AspNetCore.Mvc.ModelBinding.ModelValidationState.Invalid)
{
<div class="errors-container">
<span class="errors">@Html.ValidationMessage("OldPassword")</span>
</div>
}
@{ RenderValidation("OldPassword"); }
<input type="password" class="form-control" name="oldPassword" id="oldPassword" />
</div>
@ -151,12 +146,7 @@
<div class="form-group">
<label for="password">Password</label>
@if (ViewContext.ViewData.ModelState["Password"]?.ValidationState == Microsoft.AspNetCore.Mvc.ModelBinding.ModelValidationState.Invalid)
{
<div class="errors-container">
<span class="errors">@Html.ValidationMessage("Password")</span>
</div>
}
@{ RenderValidation("Password"); }
<input type="password" class="form-control" name="password" id="password" />
</div>
@ -164,12 +154,7 @@
<div class="form-group">
<label for="passwordConfirm">Confirm</label>
@if (ViewContext.ViewData.ModelState["PasswordConfirm"]?.ValidationState == Microsoft.AspNetCore.Mvc.ModelBinding.ModelValidationState.Invalid)
{
<div class="errors-container">
<span class="errors">@Html.ValidationMessage("PasswordConfirm")</span>
</div>
}
@{ RenderValidation("PasswordConfirm"); }
<input type="password" class="form-control" name="passwordConfirm" id="passwordConfirm" />
</div>
@ -185,12 +170,7 @@
<div class="form-group">
<label for="password">Password</label>
@if (ViewContext.ViewData.ModelState["Password"]?.ValidationState == Microsoft.AspNetCore.Mvc.ModelBinding.ModelValidationState.Invalid)
{
<div class="errors-container">
<span class="errors">@Html.ValidationMessage("Password")</span>
</div>
}
@{ RenderValidation("Password"); }
<input type="password" class="form-control" name="password" id="password" />
</div>
@ -198,12 +178,7 @@
<div class="form-group">
<label for="passwordConfirm">Confirm</label>
@if (ViewContext.ViewData.ModelState["PasswordConfirm"]?.ValidationState == Microsoft.AspNetCore.Mvc.ModelBinding.ModelValidationState.Invalid)
{
<div class="errors-container">
<span class="errors">@Html.ValidationMessage("PasswordConfirm")</span>
</div>
}
@{ RenderValidation("PasswordConfirm"); }
<input type="password" class="form-control" name="passwordConfirm" id="passwordConfirm" />
</div>
@ -219,7 +194,7 @@
<div class="profile-section">
<h2>Client</h2>
<small class="form-text text-muted mt-2 mb-2">Use the client credentials to access the API with your profile information and permissions. This can be useful when you want to create new App from code.</small>
<small class="form-text text-muted mt-2 mb-2">Use the client credentials to access the API with your profile information and permissions.</small>
<div class="row no-gutters form-group">
<div class="col-8">
@ -230,7 +205,7 @@
</div>
<div class="row no-gutters form-group">
<div class="col-8">
<label for="clientSecret">Client Secret</label>
<label for="clientSecret">Client Secret</label>
<input class="form-control" name="clientSecret" id="clientSecret" readonly value="@Model.ClientSecret" />
</div>
@ -244,25 +219,111 @@
</div>
</div>
<div class="profile-section">
<h2>Properties</h2>
<small class="form-text text-muted mt-2 mb-2">Use custom properties for rules and scripts.</small>
<form class="profile-form" asp-controller="Profile" asp-action="UpdateProperties" method="post">
<div class="mb-2" id="properties">
@for (var i = 0; i < Model.Properties.Count; i++)
{
<div class="row no-gutters form-group">
<div class="col-5 pr-2">
@{ RenderValidation($"Properties[{i}].Name"); }
<input type="text" class="form-control" asp-for="Properties[i].Name" />
</div>
<div class="col pr-2">
@{ RenderValidation($"Properties[{i}].Value"); }
<input type="text" class="form-control" asp-for="Properties[i].Value" />
</div>
<div class="col-auto">
<button type="button" class="btn btn-text-danger remove-item">
<i class="icon-bin2"></i>
</button>
</div>
</div>
}
</div>
<div class="form-group">
<button type="button" class="btn btn-text-success" id="propertyAdd">
<i class="icon-plus"></i> Add Property
</button>
</div>
<button type="submit" class="btn btn-primary">Save</button>
</form>
</div>
<script>
var propertyPlusButton = document.getElementById('propertyAdd');
var propertiesDiv = document.getElementById('properties');
var pictureButton = document.getElementById('pictureButton');
var pictureInput = document.getElementById('pictureInput');
var pictureForm = document.getElementById('pictureForm');
function updateNames() {
for (var i = 0; i < propertiesDiv.children.length; i++) {
var child = propertiesDiv.children[i];
const inputs = child.getElementsByTagName('input');
inputs[0].name = `Properties[${i}].Name`;
inputs[1].name = `Properties[${i}].Value`;
}
}
document.addEventListener('click',
function (event) {
if (event.target.className.indexOf('remove-item') >= 0) {
event.target.parentNode.parentNode.remove();
updateNames();
}
})
pictureButton.addEventListener('click',
function() {
function () {
pictureInput.click();
});
pictureInput.addEventListener('change',
function() {
function () {
pictureForm.submit();
});
propertyPlusButton.addEventListener('click',
function () {
var template = document.createElement('template');
template.innerHTML =
`<div class="row no-gutters form-group">
<div class="col-5 pr-2">
<input class="form-control" />
</div>
<div class="col pr-2">
<input class="form-control" />
</div>
<div class="col-auto">
<button type="button" class="btn btn-danger">
<i class="icon-bin"></i>
</button>
</div>
</div>`;
propertiesDiv.append(template.content.firstChild);
updateNames();
});
var successMessage = document.getElementById('success');
if (successMessage) {
setTimeout(function() {
setTimeout(function () {
successMessage.remove();
}, 5000);
}

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

@ -79,6 +79,7 @@ namespace Squidex.Config.Web
options.Filters.Add<AppResolver>();
options.Filters.Add<MeasureResultFilter>();
})
.AddRazorRuntimeCompilation()
.AddSquidexPlugins(config)
.AddSquidexSerializers();
}

1
backend/src/Squidex/Squidex.csproj

@ -42,6 +42,7 @@
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="3.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.MicrosoftAccount" Version="3.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="3.0.0" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="1.1.0" />
<PackageReference Include="Microsoft.Data.Edm" Version="5.8.4" />
<PackageReference Include="Microsoft.OData.Core" Version="7.6.3" />

Loading…
Cancel
Save