Browse Source

Feature/improve profile page (#828)

* * Better design
* Open profile in normal link.
* Fix github email scope.
* No Email merging.

* Remove unused directly and fix a second link to the profile.
pull/831/head
Sebastian Stehle 4 years ago
committed by GitHub
parent
commit
807c6a93fc
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      backend/i18n/source/backend_en.json
  2. 3
      backend/src/Squidex.Shared/Texts.it.resx
  3. 3
      backend/src/Squidex.Shared/Texts.nl.resx
  4. 5
      backend/src/Squidex.Shared/Texts.resx
  5. 3
      backend/src/Squidex.Shared/Texts.zh.resx
  6. 69
      backend/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs
  7. 10
      backend/src/Squidex/Areas/IdentityServer/Controllers/Extensions.cs
  8. 48
      backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs
  9. 18
      backend/src/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml
  10. 2
      backend/src/Squidex/Areas/IdentityServer/Views/Setup/Setup.cshtml
  11. 1
      backend/src/Squidex/Config/Authentication/GithubAuthenticationServices.cs
  12. 2
      backend/src/Squidex/wwwroot/scripts/editor-references.html
  13. 2
      frontend/src/app/framework/angular/external-link.directive.ts
  14. 23
      frontend/src/app/framework/angular/popup-link.directive.ts
  15. 1
      frontend/src/app/framework/declarations.ts
  16. 4
      frontend/src/app/framework/module.ts
  17. 4
      frontend/src/app/shell/pages/internal/profile-menu.component.html
  18. 37
      frontend/src/app/theme/_static.scss

3
backend/i18n/source/backend_en.json

@ -190,7 +190,7 @@
"contents.workflowErrorUpdate": "The workflow does not allow updates at status {status}", "contents.workflowErrorUpdate": "The workflow does not allow updates at status {status}",
"dotnet_identity_DefaultEror": "An unknown failure has occurred.", "dotnet_identity_DefaultEror": "An unknown failure has occurred.",
"dotnet_identity_DuplicateEmail": "Email is already taken.", "dotnet_identity_DuplicateEmail": "Email is already taken.",
"dotnet_identity_DuplicateUserName": "User name is already taken.", "dotnet_identity_DuplicateUserName": "Email is already taken by another user. You can add this external login to your account if you go to the profile page.",
"dotnet_identity_InvalidEmail": "Email is invalid.", "dotnet_identity_InvalidEmail": "Email is invalid.",
"dotnet_identity_InvalidUserName": "User name '{0}' is invalid, can only contain letters or digits.", "dotnet_identity_InvalidUserName": "User name '{0}' is invalid, can only contain letters or digits.",
"dotnet_identity_LoginAlreadyAssociated": "A user with this login already exists.", "dotnet_identity_LoginAlreadyAssociated": "A user with this login already exists.",
@ -250,6 +250,7 @@
"history.schemas.updated": "updated schema {[Name]}.", "history.schemas.updated": "updated schema {[Name]}.",
"history.statusChanged": "changed status of {[Schema]} content to {[Status]}.", "history.statusChanged": "changed status of {[Schema]} content to {[Status]}.",
"login.githubPrivateEmail": "Your email address is set to private in Github. Please set it to public to use Github login.", "login.githubPrivateEmail": "Your email address is set to private in Github. Please set it to public to use Github login.",
"login.noEmailAddress": "We cannot get the email address from authentication provider.",
"rules.ruleAlreadyRunning": "Another rule is already running.", "rules.ruleAlreadyRunning": "Another rule is already running.",
"schemas.dateTimeCalculatedDefaultAndDefaultError": "Calculated default value and default value cannot be used together.", "schemas.dateTimeCalculatedDefaultAndDefaultError": "Calculated default value and default value cannot be used together.",
"schemas.duplicateFieldName": "Field '{field}' has been added twice.", "schemas.duplicateFieldName": "Field '{field}' has been added twice.",

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

@ -835,6 +835,9 @@
<data name="login.githubPrivateEmail" xml:space="preserve"> <data name="login.githubPrivateEmail" xml:space="preserve">
<value>Il tuo indirizzo email è impostato su privato in Github. Impostalo come pubblico per poter utilizzare il login Github.</value> <value>Il tuo indirizzo email è impostato su privato in Github. Impostalo come pubblico per poter utilizzare il login Github.</value>
</data> </data>
<data name="login.noEmailAddress" xml:space="preserve">
<value>We cannot get the email address from authentication provider.</value>
</data>
<data name="rules.ruleAlreadyRunning" xml:space="preserve"> <data name="rules.ruleAlreadyRunning" xml:space="preserve">
<value>È in esecuzione un'altra regola.</value> <value>È in esecuzione un'altra regola.</value>
</data> </data>

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

@ -835,6 +835,9 @@
<data name="login.githubPrivateEmail" xml:space="preserve"> <data name="login.githubPrivateEmail" xml:space="preserve">
<value>Jouw e-mailadres is ingesteld op privé in Github. Stel het in op openbaar om Github-login te gebruiken.</value> <value>Jouw e-mailadres is ingesteld op privé in Github. Stel het in op openbaar om Github-login te gebruiken.</value>
</data> </data>
<data name="login.noEmailAddress" xml:space="preserve">
<value>We cannot get the email address from authentication provider.</value>
</data>
<data name="rules.ruleAlreadyRunning" xml:space="preserve"> <data name="rules.ruleAlreadyRunning" xml:space="preserve">
<value>Er wordt al een andere regel uitgevoerd.</value> <value>Er wordt al een andere regel uitgevoerd.</value>
</data> </data>

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

@ -656,7 +656,7 @@
<value>Email is already taken.</value> <value>Email is already taken.</value>
</data> </data>
<data name="dotnet_identity_DuplicateUserName" xml:space="preserve"> <data name="dotnet_identity_DuplicateUserName" xml:space="preserve">
<value>User name is already taken.</value> <value>Email is already taken by another user. You can add this external login to your account if you go to the profile page.</value>
</data> </data>
<data name="dotnet_identity_InvalidEmail" xml:space="preserve"> <data name="dotnet_identity_InvalidEmail" xml:space="preserve">
<value>Email is invalid.</value> <value>Email is invalid.</value>
@ -835,6 +835,9 @@
<data name="login.githubPrivateEmail" xml:space="preserve"> <data name="login.githubPrivateEmail" xml:space="preserve">
<value>Your email address is set to private in Github. Please set it to public to use Github login.</value> <value>Your email address is set to private in Github. Please set it to public to use Github login.</value>
</data> </data>
<data name="login.noEmailAddress" xml:space="preserve">
<value>We cannot get the email address from authentication provider.</value>
</data>
<data name="rules.ruleAlreadyRunning" xml:space="preserve"> <data name="rules.ruleAlreadyRunning" xml:space="preserve">
<value>Another rule is already running.</value> <value>Another rule is already running.</value>
</data> </data>

3
backend/src/Squidex.Shared/Texts.zh.resx

@ -835,6 +835,9 @@
<data name="login.githubPrivateEmail" xml:space="preserve"> <data name="login.githubPrivateEmail" xml:space="preserve">
<value>您的邮箱在 Github 中设置为私有。请设置为公开以使用 Github 登录。</value> <value>您的邮箱在 Github 中设置为私有。请设置为公开以使用 Github 登录。</value>
</data> </data>
<data name="login.noEmailAddress" xml:space="preserve">
<value>We cannot get the email address from authentication provider.</value>
</data>
<data name="rules.ruleAlreadyRunning" xml:space="preserve"> <data name="rules.ruleAlreadyRunning" xml:space="preserve">
<value>另一个规则已经在运行。</value> <value>另一个规则已经在运行。</value>
</data> </data>

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

@ -181,11 +181,10 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account
{ {
var provider = externalProviders[0].AuthenticationScheme; var provider = externalProviders[0].AuthenticationScheme;
var properties = var challengeRedirectUrl = Url.Action(nameof(ExternalCallback));
SignInManager.ConfigureExternalAuthenticationProperties(provider, var challengeProperties = SignInManager.ConfigureExternalAuthenticationProperties(provider, challengeRedirectUrl);
Url.Action(nameof(ExternalCallback), new { ReturnUrl = returnUrl }));
return Challenge(properties, provider); return Challenge(challengeProperties, provider);
} }
var vm = new LoginVM var vm = new LoginVM
@ -204,25 +203,24 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account
[Route("account/external/")] [Route("account/external/")]
public IActionResult External(string provider, string? returnUrl = null) public IActionResult External(string provider, string? returnUrl = null)
{ {
var properties = var challengeRedirectUrl = Url.Action(nameof(ExternalCallback), new { returnUrl });
SignInManager.ConfigureExternalAuthenticationProperties(provider, var challengeProperties = SignInManager.ConfigureExternalAuthenticationProperties(provider, challengeRedirectUrl);
Url.Action(nameof(ExternalCallback), new { ReturnUrl = returnUrl }));
return Challenge(properties, provider); return Challenge(challengeProperties, provider);
} }
[HttpGet] [HttpGet]
[Route("account/external-callback/")] [Route("account/external-callback/")]
public async Task<IActionResult> ExternalCallback(string? returnUrl = null) public async Task<IActionResult> ExternalCallback(string? returnUrl = null)
{ {
var externalLogin = await SignInManager.GetExternalLoginInfoWithDisplayNameAsync(); var login = await SignInManager.GetExternalLoginInfoWithDisplayNameAsync();
if (externalLogin == null) if (login == null)
{ {
return RedirectToAction(nameof(Login)); return RedirectToAction(nameof(Login));
} }
var result = await SignInManager.ExternalLoginSignInAsync(externalLogin.LoginProvider, externalLogin.ProviderKey, true); var result = await SignInManager.ExternalLoginSignInAsync(login.LoginProvider, login.ProviderKey, true);
if (!result.Succeeded && result.IsLockedOut) if (!result.Succeeded && result.IsLockedOut)
{ {
@ -235,42 +233,33 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account
if (isLoggedIn) if (isLoggedIn)
{ {
user = await userService.FindByLoginAsync(externalLogin.LoginProvider, externalLogin.ProviderKey, HttpContext.RequestAborted); user = await userService.FindByLoginAsync(login.LoginProvider, login.ProviderKey, HttpContext.RequestAborted);
} }
else else
{ {
var email = externalLogin.Principal.GetEmail(); var email = login.Principal.GetEmail();
if (string.IsNullOrWhiteSpace(email)) if (string.IsNullOrWhiteSpace(email))
{ {
throw new DomainException("User has no exposed email address."); throw new DomainException(T.Get("login.noEmailAddress"));
} }
user = await userService.FindByEmailAsync(email, HttpContext.RequestAborted); var values = new UserValues
if (user != null)
{
var update = CreateUserValues(externalLogin, email, user);
await userService.UpdateAsync(user.Id, update, ct: HttpContext.RequestAborted);
}
else
{ {
var update = CreateUserValues(externalLogin, email); CustomClaims = login.Principal.Claims.GetSquidexClaims().ToList()
};
user = await userService.CreateAsync(email, update, identityOptions.LockAutomatically, HttpContext.RequestAborted); user = await userService.CreateAsync(email, values, identityOptions.LockAutomatically, HttpContext.RequestAborted);
}
await userService.AddLoginAsync(user.Id, externalLogin, HttpContext.RequestAborted); await userService.AddLoginAsync(user.Id, login,
HttpContext.RequestAborted);
var (success, locked) = await LoginAsync(externalLogin); (isLoggedIn, var locked) = await LoginAsync(login);
if (locked) if (locked)
{ {
return View(nameof(LockedOut)); return View(nameof(LockedOut));
} }
isLoggedIn = success;
} }
if (!isLoggedIn) if (!isLoggedIn)
@ -287,26 +276,6 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account
} }
} }
private static UserValues CreateUserValues(ExternalLoginInfo externalLogin, string email, IUser? user = null)
{
var values = new UserValues
{
CustomClaims = externalLogin.Principal.Claims.GetSquidexClaims().ToList()
};
if (user != null && !user.Claims.HasPictureUrl())
{
values.PictureUrl = GravatarHelper.CreatePictureUrl(email);
}
if (user != null && !user.Claims.HasDisplayName())
{
values.DisplayName = email;
}
return values;
}
private async Task<(bool Success, bool Locked)> LoginAsync(UserLoginInfo externalLogin) private async Task<(bool Success, bool Locked)> LoginAsync(UserLoginInfo externalLogin)
{ {
var result = await SignInManager.ExternalLoginSignInAsync(externalLogin.LoginProvider, externalLogin.ProviderKey, true); var result = await SignInManager.ExternalLoginSignInAsync(externalLogin.LoginProvider, externalLogin.ProviderKey, true);

10
backend/src/Squidex/Areas/IdentityServer/Controllers/Extensions.cs

@ -16,23 +16,23 @@ namespace Squidex.Areas.IdentityServer.Controllers
{ {
public static async Task<ExternalLoginInfo> GetExternalLoginInfoWithDisplayNameAsync(this SignInManager<IdentityUser> signInManager, string? expectedXsrf = null) public static async Task<ExternalLoginInfo> GetExternalLoginInfoWithDisplayNameAsync(this SignInManager<IdentityUser> signInManager, string? expectedXsrf = null)
{ {
var externalLogin = await signInManager.GetExternalLoginInfoAsync(expectedXsrf); var login = await signInManager.GetExternalLoginInfoAsync(expectedXsrf);
if (externalLogin == null) if (login == null)
{ {
throw new InvalidOperationException("Request from external provider cannot be handled."); throw new InvalidOperationException("Request from external provider cannot be handled.");
} }
var email = externalLogin.Principal.GetEmail(); var email = login.Principal.GetEmail();
if (string.IsNullOrWhiteSpace(email)) if (string.IsNullOrWhiteSpace(email))
{ {
throw new InvalidOperationException("External provider does not provide email claim."); throw new InvalidOperationException("External provider does not provide email claim.");
} }
externalLogin.ProviderDisplayName = email; login.ProviderDisplayName = email;
return externalLogin; return login;
} }
public static async Task<List<ExternalProvider>> GetExternalProvidersAsync(this SignInManager<IdentityUser> signInManager) public static async Task<List<ExternalProvider>> GetExternalProvidersAsync(this SignInManager<IdentityUser> signInManager)

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

@ -59,18 +59,19 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
{ {
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
var properties = var userId = userService.GetUserId(User, HttpContext.RequestAborted);
SignInManager.ConfigureExternalAuthenticationProperties(provider,
Url.Action(nameof(AddLoginCallback)), userService.GetUserId(User, HttpContext.RequestAborted));
return Challenge(properties, provider); var challengeRedirectUrl = Url.Action(nameof(AddLoginCallback));
var challengeProperties = SignInManager.ConfigureExternalAuthenticationProperties(provider, userId);
return Challenge(challengeProperties, provider);
} }
[HttpGet] [HttpGet]
[Route("/account/profile/login-add-callback/")] [Route("/account/profile/login-add-callback/")]
public Task<IActionResult> AddLoginCallback() public Task<IActionResult> AddLoginCallback()
{ {
return MakeChangeAsync(u => AddLoginAsync(u), return MakeChangeAsync((id, ct) => AddLoginAsync(id, ct),
T.Get("users.profile.addLoginDone"), None.Value); T.Get("users.profile.addLoginDone"), None.Value);
} }
@ -78,7 +79,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
[Route("/account/profile/update/")] [Route("/account/profile/update/")]
public Task<IActionResult> UpdateProfile(ChangeProfileModel model) public Task<IActionResult> UpdateProfile(ChangeProfileModel model)
{ {
return MakeChangeAsync(id => userService.UpdateAsync(id, model.ToValues(), ct: HttpContext.RequestAborted), return MakeChangeAsync((id, ct) => userService.UpdateAsync(id, model.ToValues(), ct: ct),
T.Get("users.profile.updateProfileDone"), model); T.Get("users.profile.updateProfileDone"), model);
} }
@ -86,7 +87,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
[Route("/account/profile/properties/")] [Route("/account/profile/properties/")]
public Task<IActionResult> UpdateProperties(ChangePropertiesModel model) public Task<IActionResult> UpdateProperties(ChangePropertiesModel model)
{ {
return MakeChangeAsync(id => userService.UpdateAsync(id, model.ToValues(), ct: HttpContext.RequestAborted), return MakeChangeAsync((id, ct) => userService.UpdateAsync(id, model.ToValues(), ct: ct),
T.Get("users.profile.updatePropertiesDone"), model); T.Get("users.profile.updatePropertiesDone"), model);
} }
@ -94,7 +95,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
[Route("/account/profile/login-remove/")] [Route("/account/profile/login-remove/")]
public Task<IActionResult> RemoveLogin(RemoveLoginModel model) public Task<IActionResult> RemoveLogin(RemoveLoginModel model)
{ {
return MakeChangeAsync(id => userService.RemoveLoginAsync(id, model.LoginProvider, model.ProviderKey, HttpContext.RequestAborted), return MakeChangeAsync((id, ct) => userService.RemoveLoginAsync(id, model.LoginProvider, model.ProviderKey, ct),
T.Get("users.profile.removeLoginDone"), model); T.Get("users.profile.removeLoginDone"), model);
} }
@ -102,7 +103,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
[Route("/account/profile/password-set/")] [Route("/account/profile/password-set/")]
public Task<IActionResult> SetPassword(SetPasswordModel model) public Task<IActionResult> SetPassword(SetPasswordModel model)
{ {
return MakeChangeAsync(id => userService.SetPasswordAsync(id, model.Password, ct: HttpContext.RequestAborted), return MakeChangeAsync((id, ct) => userService.SetPasswordAsync(id, model.Password, ct: ct),
T.Get("users.profile.setPasswordDone"), model); T.Get("users.profile.setPasswordDone"), model);
} }
@ -110,7 +111,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
[Route("/account/profile/password-change/")] [Route("/account/profile/password-change/")]
public Task<IActionResult> ChangePassword(ChangePasswordModel model) public Task<IActionResult> ChangePassword(ChangePasswordModel model)
{ {
return MakeChangeAsync(id => userService.SetPasswordAsync(id, model.Password, model.OldPassword, HttpContext.RequestAborted), return MakeChangeAsync((id, ct) => userService.SetPasswordAsync(id, model.Password, model.OldPassword, ct),
T.Get("users.profile.changePasswordDone"), model); T.Get("users.profile.changePasswordDone"), model);
} }
@ -118,7 +119,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
[Route("/account/profile/generate-client-secret/")] [Route("/account/profile/generate-client-secret/")]
public Task<IActionResult> GenerateClientSecret() public Task<IActionResult> GenerateClientSecret()
{ {
return MakeChangeAsync(id => GenerateClientSecretAsync(id), return MakeChangeAsync((id, ct) => GenerateClientSecretAsync(id, ct),
T.Get("users.profile.generateClientDone"), None.Value); T.Get("users.profile.generateClientDone"), None.Value);
} }
@ -126,39 +127,42 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
[Route("/account/profile/upload-picture/")] [Route("/account/profile/upload-picture/")]
public Task<IActionResult> UploadPicture(List<IFormFile> file) public Task<IActionResult> UploadPicture(List<IFormFile> file)
{ {
return MakeChangeAsync(user => UpdatePictureAsync(file, user), return MakeChangeAsync((id, ct) => UpdatePictureAsync(file, id, ct),
T.Get("users.profile.uploadPictureDone"), None.Value); T.Get("users.profile.uploadPictureDone"), None.Value);
} }
private async Task GenerateClientSecretAsync(string id) private async Task GenerateClientSecretAsync(string id,
CancellationToken ct)
{ {
var update = new UserValues { ClientSecret = RandomHash.New() }; var update = new UserValues { ClientSecret = RandomHash.New() };
await userService.UpdateAsync(id, update, ct: HttpContext.RequestAborted); await userService.UpdateAsync(id, update, ct: ct);
} }
private async Task AddLoginAsync(string id) private async Task AddLoginAsync(string id,
CancellationToken ct)
{ {
var externalLogin = await SignInManager.GetExternalLoginInfoWithDisplayNameAsync(id); var login = await SignInManager.GetExternalLoginInfoWithDisplayNameAsync(id);
await userService.AddLoginAsync(id, externalLogin, HttpContext.RequestAborted); await userService.AddLoginAsync(id, login, ct);
} }
private async Task UpdatePictureAsync(List<IFormFile> files, string id) private async Task UpdatePictureAsync(List<IFormFile> files, string id,
CancellationToken ct)
{ {
if (files.Count != 1) if (files.Count != 1)
{ {
throw new ValidationException(T.Get("validation.onlyOneFile")); throw new ValidationException(T.Get("validation.onlyOneFile"));
} }
await UploadResizedAsync(files[0], id, HttpContext.RequestAborted); await UploadResizedAsync(files[0], id, ct);
var update = new UserValues var update = new UserValues
{ {
PictureUrl = SquidexClaimTypes.PictureUrlStore PictureUrl = SquidexClaimTypes.PictureUrlStore
}; };
await userService.UpdateAsync(id, update, ct: HttpContext.RequestAborted); await userService.UpdateAsync(id, update, ct: ct);
} }
private async Task UploadResizedAsync(IFormFile file, string id, private async Task UploadResizedAsync(IFormFile file, string id,
@ -193,7 +197,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
} }
} }
private async Task<IActionResult> MakeChangeAsync<TModel>(Func<string, Task> action, string successMessage, TModel? model = null) where TModel : class private async Task<IActionResult> MakeChangeAsync<TModel>(Func<string, CancellationToken, Task> action, string successMessage, TModel? model = null) where TModel : class
{ {
var user = await userService.GetAsync(User, HttpContext.RequestAborted); var user = await userService.GetAsync(User, HttpContext.RequestAborted);
@ -210,7 +214,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
string errorMessage; string errorMessage;
try try
{ {
await action(user.Id); await action(user.Id, HttpContext.RequestAborted);
await SignInManager.SignInAsync((IdentityUser)user.Identity, true); await SignInManager.SignInAsync((IdentityUser)user.Identity, true);

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

@ -8,7 +8,7 @@
@if (ViewContext.ViewData.ModelState[field]?.ValidationState == Microsoft.AspNetCore.Mvc.ModelBinding.ModelValidationState.Invalid) @if (ViewContext.ViewData.ModelState[field]?.ValidationState == Microsoft.AspNetCore.Mvc.ModelBinding.ModelValidationState.Invalid)
{ {
<div class="errors-container"> <div class="errors-container">
<span class="errors">Html.ValidationMessage(field)</span> <span class="errors">@Html.ValidationMessage(field)</span>
</div> </div>
} }
} }
@ -77,6 +77,8 @@
@if (Model!.ExternalProviders.Any()) @if (Model!.ExternalProviders.Any())
{ {
<hr />
<div class="profile-section"> <div class="profile-section">
<h2>@T.Get("users.profile.loginsTitle")</h2> <h2>@T.Get("users.profile.loginsTitle")</h2>
@ -127,6 +129,8 @@
@if (Model!.HasPasswordAuth) @if (Model!.HasPasswordAuth)
{ {
<hr />
<div class="profile-section"> <div class="profile-section">
<h2>@T.Get("users.profile.passwordTitle")</h2> <h2>@T.Get("users.profile.passwordTitle")</h2>
@ -189,19 +193,21 @@
</div> </div>
} }
<hr />
<div class="profile-section"> <div class="profile-section">
<h2>@T.Get("users.profile.clientTitle")</h2> <h2>@T.Get("users.profile.clientTitle")</h2>
<small class="form-text text-muted mt-2 mb-2">@T.Get("users.profile.clientHint")</small> <small class="form-text text-muted mt-2 mb-2">@T.Get("users.profile.clientHint")</small>
<div class="row no-gutters form-group"> <div class="row g-2 form-group">
<div class="col-8"> <div class="col-8">
<label for="clientId">@T.Get("common.clientId")</label> <label for="clientId">@T.Get("common.clientId")</label>
<input class="form-control" name="clientId" id="clientId" readonly value="@Model!.Id" /> <input class="form-control" name="clientId" id="clientId" readonly value="@Model!.Id" />
</div> </div>
</div> </div>
<div class="row no-gutters form-group"> <div class="row g-2 form-group">
<div class="col-8"> <div class="col-8">
<label for="clientSecret">@T.Get("common.clientSecret")</label> <label for="clientSecret">@T.Get("common.clientSecret")</label>
@ -217,6 +223,8 @@
</div> </div>
</div> </div>
<hr />
<div class="profile-section"> <div class="profile-section">
<h2>@T.Get("users.profile.propertiesTitle")</h2> <h2>@T.Get("users.profile.propertiesTitle")</h2>
@ -226,7 +234,7 @@
<div class="mb-2" id="properties"> <div class="mb-2" id="properties">
@for (var i = 0; i < Model!.Properties.Count; i++) @for (var i = 0; i < Model!.Properties.Count; i++)
{ {
<div class="row no-gutters form-group"> <div class="row g-2 form-group">
<div class="col-5 pr-2"> <div class="col-5 pr-2">
@{ RenderValidation($"Properties[{i}].Name"); } @{ RenderValidation($"Properties[{i}].Name"); }
@ -299,7 +307,7 @@
var template = document.createElement('template'); var template = document.createElement('template');
template.innerHTML = template.innerHTML =
`<div class="row no-gutters form-group"> `<div class="row g-2 form-group">
<div class="col-5 pr-2"> <div class="col-5 pr-2">
<input class="form-control" /> <input class="form-control" />
</div> </div>

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

@ -8,7 +8,7 @@
@if (ViewContext.ViewData.ModelState[field]?.ValidationState == Microsoft.AspNetCore.Mvc.ModelBinding.ModelValidationState.Invalid) @if (ViewContext.ViewData.ModelState[field]?.ValidationState == Microsoft.AspNetCore.Mvc.ModelBinding.ModelValidationState.Invalid)
{ {
<div class="errors-container"> <div class="errors-container">
<span class="errors">Html.ValidationMessage(field)</span> <span class="errors">@Html.ValidationMessage(field)</span>
</div> </div>
} }
} }

1
backend/src/Squidex/Config/Authentication/GithubAuthenticationServices.cs

@ -20,6 +20,7 @@ namespace Squidex.Config.Authentication
options.ClientId = identityOptions.GithubClient; options.ClientId = identityOptions.GithubClient;
options.ClientSecret = identityOptions.GithubSecret; options.ClientSecret = identityOptions.GithubSecret;
options.Events = new GithubHandler(); options.Events = new GithubHandler();
options.Scope.Add("user:email");
}); });
} }

2
backend/src/Squidex/wwwroot/scripts/editor-references.html

@ -19,7 +19,7 @@
<body> <body>
<select class="form-select" id="editor"> <select class="form-select" id="editor">
<option></option> <option></option>
</textarea> </select>
<script> <script>
const element = document.getElementById('editor'); const element = document.getElementById('editor');

2
frontend/src/app/framework/angular/external-link.directive.ts

@ -12,7 +12,7 @@ import { AfterViewInit, Directive, ElementRef, Input, Renderer2 } from '@angular
}) })
export class ExternalLinkDirective implements AfterViewInit { export class ExternalLinkDirective implements AfterViewInit {
@Input('sqxExternalLink') @Input('sqxExternalLink')
public type?: string; public type?: 'noicon' | 'default' | string;
constructor( constructor(
private readonly element: ElementRef<Element>, private readonly element: ElementRef<Element>,

23
frontend/src/app/framework/angular/popup-link.directive.ts

@ -1,23 +0,0 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Directive, HostListener, Input } from '@angular/core';
@Directive({
selector: '[sqxPopupLink]',
})
export class PopupLinkDirective {
@Input('sqxPopupLink')
public url!: string;
@HostListener('click')
public onClick(): boolean {
window.open(this.url, '_target', 'location=no,toolbar=no,width=500,height=500,left=100,top=100;');
return false;
}
}

1
frontend/src/app/framework/declarations.ts

@ -65,7 +65,6 @@ export * from './angular/pipes/money.pipe';
export * from './angular/pipes/name.pipe'; export * from './angular/pipes/name.pipe';
export * from './angular/pipes/numbers.pipes'; export * from './angular/pipes/numbers.pipes';
export * from './angular/pipes/translate.pipe'; export * from './angular/pipes/translate.pipe';
export * from './angular/popup-link.directive';
export * from './angular/resized.directive'; export * from './angular/resized.directive';
export * from './angular/routers/can-deactivate.guard'; export * from './angular/routers/can-deactivate.guard';
export * from './angular/routers/parent-link.directive'; export * from './angular/routers/parent-link.directive';

4
frontend/src/app/framework/module.ts

@ -11,7 +11,7 @@ import { ModuleWithProviders, NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { ColorPickerModule } from 'ngx-color-picker'; import { ColorPickerModule } from 'ngx-color-picker';
import { AnalyticsService, AutocompleteComponent, AvatarComponent, CachingInterceptor, CanDeactivateGuard, CheckboxGroupComponent, ClipboardService, CodeComponent, CodeEditorComponent, ColorPickerComponent, ConfirmClickDirective, ControlErrorsComponent, ControlErrorsMessagesComponent, CopyDirective, DarkenPipe, DatePipe, DateTimeEditorComponent, DayOfWeekPipe, DayPipe, DialogRendererComponent, DialogService, DisplayNamePipe, DropdownComponent, DropdownMenuComponent, DurationPipe, EditableTitleComponent, ExternalLinkDirective, FileDropDirective, FileSizePipe, FocusOnInitDirective, FormAlertComponent, FormErrorComponent, FormHintComponent, FromNowPipe, FullDateTimePipe, HighlightPipe, HoverBackgroundDirective, ImageSourceDirective, IndeterminateValueDirective, ISODatePipe, KeysPipe, KNumberPipe, LanguageSelectorComponent, LayoutComponent, LayoutContainerDirective, LightenPipe, ListViewComponent, LoadingInterceptor, LoadingService, LocalizedInputComponent, LocalStoreService, MarkdownDirective, MarkdownInlinePipe, MarkdownPipe, MessageBus, ModalDialogComponent, ModalDirective, ModalPlacementDirective, MoneyPipe, MonthPipe, OnboardingService, OnboardingTooltipComponent, PagerComponent, ParentLinkDirective, PopupLinkDirective, ProgressBarComponent, ResizedDirective, ResizeService, ResourceLoaderService, RootViewComponent, SafeHtmlPipe, SafeResourceUrlPipe, SafeUrlPipe, ScrollActiveDirective, ShortcutComponent, ShortcutDirective, ShortcutService, ShortDatePipe, ShortTimePipe, StarsComponent, StatusIconComponent, StopClickDirective, StopDragDirective, SyncScollingDirective, SyncWidthDirective, TabRouterlinkDirective, TagEditorComponent, TemplateWrapperDirective, TempService, TitleComponent, TitleService, ToggleComponent, ToolbarComponent, TooltipDirective, TransformInputDirective, TranslatePipe, VideoPlayerComponent } from './declarations'; import { AnalyticsService, AutocompleteComponent, AvatarComponent, CachingInterceptor, CanDeactivateGuard, CheckboxGroupComponent, ClipboardService, CodeComponent, CodeEditorComponent, ColorPickerComponent, ConfirmClickDirective, ControlErrorsComponent, ControlErrorsMessagesComponent, CopyDirective, DarkenPipe, DatePipe, DateTimeEditorComponent, DayOfWeekPipe, DayPipe, DialogRendererComponent, DialogService, DisplayNamePipe, DropdownComponent, DropdownMenuComponent, DurationPipe, EditableTitleComponent, ExternalLinkDirective, FileDropDirective, FileSizePipe, FocusOnInitDirective, FormAlertComponent, FormErrorComponent, FormHintComponent, FromNowPipe, FullDateTimePipe, HighlightPipe, HoverBackgroundDirective, ImageSourceDirective, IndeterminateValueDirective, ISODatePipe, KeysPipe, KNumberPipe, LanguageSelectorComponent, LayoutComponent, LayoutContainerDirective, LightenPipe, ListViewComponent, LoadingInterceptor, LoadingService, LocalizedInputComponent, LocalStoreService, MarkdownDirective, MarkdownInlinePipe, MarkdownPipe, MessageBus, ModalDialogComponent, ModalDirective, ModalPlacementDirective, MoneyPipe, MonthPipe, OnboardingService, OnboardingTooltipComponent, PagerComponent, ParentLinkDirective, ProgressBarComponent, ResizedDirective, ResizeService, ResourceLoaderService, RootViewComponent, SafeHtmlPipe, SafeResourceUrlPipe, SafeUrlPipe, ScrollActiveDirective, ShortcutComponent, ShortcutDirective, ShortcutService, ShortDatePipe, ShortTimePipe, StarsComponent, StatusIconComponent, StopClickDirective, StopDragDirective, SyncScollingDirective, SyncWidthDirective, TabRouterlinkDirective, TagEditorComponent, TemplateWrapperDirective, TempService, TitleComponent, TitleService, ToggleComponent, ToolbarComponent, TooltipDirective, TransformInputDirective, TranslatePipe, VideoPlayerComponent } from './declarations';
@NgModule({ @NgModule({
imports: [ imports: [
@ -76,7 +76,6 @@ import { AnalyticsService, AutocompleteComponent, AvatarComponent, CachingInterc
OnboardingTooltipComponent, OnboardingTooltipComponent,
PagerComponent, PagerComponent,
ParentLinkDirective, ParentLinkDirective,
PopupLinkDirective,
ProgressBarComponent, ProgressBarComponent,
ResizedDirective, ResizedDirective,
RootViewComponent, RootViewComponent,
@ -161,7 +160,6 @@ import { AnalyticsService, AutocompleteComponent, AvatarComponent, CachingInterc
OnboardingTooltipComponent, OnboardingTooltipComponent,
PagerComponent, PagerComponent,
ParentLinkDirective, ParentLinkDirective,
PopupLinkDirective,
ProgressBarComponent, ProgressBarComponent,
ReactiveFormsModule, ReactiveFormsModule,
ResizedDirective, ResizedDirective,

4
frontend/src/app/shell/pages/internal/profile-menu.component.html

@ -10,7 +10,7 @@
<ng-container *sqxModal="modalMenu;onRoot:false;closeAlways:true"> <ng-container *sqxModal="modalMenu;onRoot:false;closeAlways:true">
<sqx-dropdown-menu [sqxAnchoredTo]="button" [offsetY]="10"> <sqx-dropdown-menu [sqxAnchoredTo]="button" [offsetY]="10">
<a class="dropdown-item dropdown-info" [sqxPopupLink]="snapshot.profileUrl"> <a class="dropdown-item dropdown-info" href="{{snapshot.profileUrl}}" sqxExternalLink="noicon">
<div>{{ 'profile.userEmail' | sqxTranslate }}</div> <div>{{ 'profile.userEmail' | sqxTranslate }}</div>
<strong>{{snapshot.profileEmail}}</strong> <strong>{{snapshot.profileEmail}}</strong>
@ -22,7 +22,7 @@
{{ 'common.administration' | sqxTranslate }} {{ 'common.administration' | sqxTranslate }}
</a> </a>
<a class="dropdown-item" [sqxPopupLink]="snapshot.profileUrl"> <a class="dropdown-item" href="{{snapshot.profileUrl}}" sqxExternalLink="noicon">
{{ 'profile.title' | sqxTranslate }} {{ 'profile.title' | sqxTranslate }}
</a> </a>

37
frontend/src/app/theme/_static.scss

@ -48,8 +48,10 @@ noscript {
// Fix logo on the top right corner of the screen. // Fix logo on the top right corner of the screen.
&-logo { &-logo {
@include absolute(1rem, 1rem, auto, auto); display: block;
height: 1.75rem; margin-left: auto;
margin-bottom: 1rem;
height: 2.5rem;
} }
&-password-signup { &-password-signup {
@ -57,7 +59,7 @@ noscript {
} }
&-section { &-section {
margin-top: 3rem; margin-top: 2rem;
} }
&-section-sm { &-section-sm {
@ -94,7 +96,10 @@ noscript {
border-bottom: 1px solid $color-border; border-bottom: 1px solid $color-border;
margin-bottom: 2.5rem; margin-bottom: 2.5rem;
margin-top: 1.5rem; margin-top: 1.5rem;
margin-left: auto;
margin-right: auto;
text-align: center; text-align: center;
width: 7rem;
} }
&-text { &-text {
@ -108,10 +113,36 @@ noscript {
} }
} }
h1 {
font-size: 1.75rem;
font-weight: 400;
}
hr {
margin-top: 2rem;
margin-left: auto;
margin-right: auto;
width: 7rem;
}
.card { .card {
border-width: 1px; border-width: 1px;
} }
.card-body {
padding: 1.5rem;
}
.table {
tbody {
border-top: 1px solid $color-border;
}
tr {
border-top: 0;
}
}
label { label {
.card { .card {
font-weight: normal; font-weight: normal;

Loading…
Cancel
Save