Browse Source

Improved privacy by hiding the email of the user.

pull/264/head
Sebastian Stehle 9 years ago
parent
commit
af645a420b
  1. 4
      src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs
  2. 23
      src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs
  3. 12
      src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs
  4. 10
      src/Squidex.Domain.Users/UserExtensions.cs
  5. 3
      src/Squidex.Domain.Users/UserManagerExtensions.cs
  6. 2
      src/Squidex.Shared/Identity/SquidexClaimTypes.cs
  7. 2
      src/Squidex.Shared/Users/IUserResolver.cs
  8. 11
      src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs
  9. 1
      src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs
  10. 2
      src/Squidex/Areas/Api/Controllers/Apps/Models/AssignAppContributorDto.cs
  11. 20
      src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorAssignedDto.cs
  12. 2
      src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs
  13. 9
      src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs
  14. 1
      src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs
  15. 9
      src/Squidex/Areas/Api/Controllers/Users/Models/CreateUserDto.cs
  16. 26
      src/Squidex/Areas/Api/Controllers/Users/Models/PublicUserDto.cs
  17. 9
      src/Squidex/Areas/Api/Controllers/Users/Models/UpdateUserDto.cs
  18. 6
      src/Squidex/Areas/Api/Controllers/Users/Models/UserCreatedDto.cs
  19. 6
      src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs
  20. 4
      src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs
  21. 8
      src/Squidex/Areas/Api/Controllers/Users/UsersController.cs
  22. 2
      src/Squidex/Areas/IdentityServer/Controllers/Profile/ChangeProfileModel.cs
  23. 3
      src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs
  24. 2
      src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileVM.cs
  25. 12
      src/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml
  26. 1
      src/Squidex/app/features/administration/pages/users/user-page.component.ts
  27. 14
      src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html
  28. 11
      src/Squidex/app/features/settings/pages/contributors/contributors-page.component.scss
  29. 13
      src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts
  30. 7
      src/Squidex/app/framework/angular/autocomplete.component.ts
  31. 40
      src/Squidex/app/shared/components/pipes.ts
  32. 6
      src/Squidex/app/shared/module.ts
  33. 13
      src/Squidex/app/shared/services/app-contributors.service.spec.ts
  34. 16
      src/Squidex/app/shared/services/app-contributors.service.ts
  35. 20
      src/Squidex/app/shared/services/users-provider.service.spec.ts
  36. 10
      src/Squidex/app/shared/services/users-provider.service.ts
  37. 68
      src/Squidex/app/shared/services/users.service.spec.ts
  38. 37
      src/Squidex/app/shared/services/users.service.ts
  39. 12
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs
  40. 34
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppContributorsTests.cs
  41. 2
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppTests.cs

4
src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs

@ -70,11 +70,13 @@ namespace Squidex.Domain.Apps.Entities.Apps
}); });
case AssignContributor assigneContributor: case AssignContributor assigneContributor:
return UpdateAsync(assigneContributor, async c => return UpdateReturnAsync(assigneContributor, async c =>
{ {
await GuardAppContributors.CanAssign(Snapshot.Contributors, c, userResolver, appPlansProvider.GetPlan(Snapshot.Plan?.PlanId)); await GuardAppContributors.CanAssign(Snapshot.Contributors, c, userResolver, appPlansProvider.GetPlan(Snapshot.Plan?.PlanId));
AssignContributor(c); AssignContributor(c);
return EntityCreatedResult.Create(c.ContributorId, NewVersion);
}); });
case RemoveContributor removeContributor: case RemoveContributor removeContributor:

23
src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs

@ -34,20 +34,27 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
} }
else else
{ {
if (await users.FindByIdAsync(command.ContributorId) == null) var user = await users.FindByIdOrEmailAsync(command.ContributorId);
if (user == null)
{ {
error(new ValidationError("Cannot find contributor id.", nameof(command.ContributorId))); error(new ValidationError("Cannot find contributor id.", nameof(command.ContributorId)));
} }
else if (contributors.TryGetValue(command.ContributorId, out var existing)) else
{ {
if (existing == command.Permission) command.ContributorId = user.Id;
if (contributors.TryGetValue(command.ContributorId, out var existing))
{ {
error(new ValidationError("Contributor has already this permission.", nameof(command.Permission))); if (existing == command.Permission)
{
error(new ValidationError("Contributor has already this permission.", nameof(command.Permission)));
}
}
else if (plan.MaxContributors == contributors.Count)
{
error(new ValidationError("You have reached the maximum number of contributors for your plan."));
} }
}
else if (plan.MaxContributors == contributors.Count)
{
error(new ValidationError("You have reached the maximum number of contributors for your plan."));
} }
} }
}); });

12
src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs

@ -12,6 +12,7 @@ using System.Security.Claims;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using MongoDB.Bson;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.Tasks; using Squidex.Infrastructure.Tasks;
@ -72,9 +73,16 @@ namespace Squidex.Domain.Users.MongoDb
return new MongoUser { Email = email, UserName = email }; return new MongoUser { Email = email, UserName = email };
} }
public async Task<IUser> FindByIdAsync(string id) public async Task<IUser> FindByIdOrEmailAsync(string id)
{ {
return await Collection.Find(x => x.Id == id).FirstOrDefaultAsync(); if (ObjectId.TryParse(id, out var parsed))
{
return await Collection.Find(x => x.Id == id).FirstOrDefaultAsync();
}
else
{
return await Collection.Find(x => x.NormalizedEmail == id.ToUpperInvariant()).FirstOrDefaultAsync();
}
} }
public async Task<IUser> FindByIdAsync(string userId, CancellationToken cancellationToken) public async Task<IUser> FindByIdAsync(string userId, CancellationToken cancellationToken)

10
src/Squidex.Domain.Users/UserExtensions.cs

@ -35,6 +35,11 @@ namespace Squidex.Domain.Users
user.SetClaim(SquidexClaimTypes.SquidexPictureUrl, GravatarHelper.CreatePictureUrl(email)); user.SetClaim(SquidexClaimTypes.SquidexPictureUrl, GravatarHelper.CreatePictureUrl(email));
} }
public static void SetHidden(this IUser user, bool value)
{
user.SetClaim(SquidexClaimTypes.SquidexHidden, value.ToString());
}
public static void SetConsent(this IUser user) public static void SetConsent(this IUser user)
{ {
user.SetClaim(SquidexClaimTypes.SquidexConsent, "true"); user.SetClaim(SquidexClaimTypes.SquidexConsent, "true");
@ -45,6 +50,11 @@ namespace Squidex.Domain.Users
user.SetClaim(SquidexClaimTypes.SquidexConsentForEmails, value.ToString()); user.SetClaim(SquidexClaimTypes.SquidexConsentForEmails, value.ToString());
} }
public static bool IsHidden(this IUser user)
{
return user.HasClaimValue(SquidexClaimTypes.SquidexHidden, "true");
}
public static bool HasConsent(this IUser user) public static bool HasConsent(this IUser user)
{ {
return user.HasClaimValue(SquidexClaimTypes.SquidexConsent, "true"); return user.HasClaimValue(SquidexClaimTypes.SquidexConsent, "true");

3
src/Squidex.Domain.Users/UserManagerExtensions.cs

@ -71,8 +71,9 @@ namespace Squidex.Domain.Users
return user; return user;
} }
public static Task<IdentityResult> UpdateAsync(this UserManager<IUser> userManager, IUser user, string email, string displayName) public static Task<IdentityResult> UpdateAsync(this UserManager<IUser> userManager, IUser user, string email, string displayName, bool hidden)
{ {
user.SetHidden(hidden);
user.SetEmail(email); user.SetEmail(email);
user.SetDisplayName(displayName); user.SetDisplayName(displayName);

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

@ -17,6 +17,8 @@ namespace Squidex.Shared.Identity
public static readonly string SquidexConsentForEmails = "urn:squidex:consent:emails"; public static readonly string SquidexConsentForEmails = "urn:squidex:consent:emails";
public static readonly string SquidexHidden = "urn:squidex:hidden";
public static readonly string Prefix = "urn:squidex:"; public static readonly string Prefix = "urn:squidex:";
} }
} }

2
src/Squidex.Shared/Users/IUserResolver.cs

@ -11,6 +11,6 @@ namespace Squidex.Shared.Users
{ {
public interface IUserResolver public interface IUserResolver
{ {
Task<IUser> FindByIdAsync(string id); Task<IUser> FindByIdOrEmailAsync(string idOrEmail);
} }
} }

11
src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs

@ -65,19 +65,24 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="request">Contributor object that needs to be added to the app.</param> /// <param name="request">Contributor object that needs to be added to the app.</param>
/// <returns> /// <returns>
/// 204 => User assigned to app. /// 200 => User assigned to app.
/// 400 => User is already assigned to the app or not found. /// 400 => User is already assigned to the app or not found.
/// 404 => App not found. /// 404 => App not found.
/// </returns> /// </returns>
[HttpPost] [HttpPost]
[Route("apps/{app}/contributors/")] [Route("apps/{app}/contributors/")]
[ProducesResponseType(typeof(ContributorAssignedDto), 201)]
[ProducesResponseType(typeof(ErrorDto), 400)] [ProducesResponseType(typeof(ErrorDto), 400)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> PostContributor(string app, [FromBody] AssignAppContributorDto request) public async Task<IActionResult> PostContributor(string app, [FromBody] AssignAppContributorDto request)
{ {
await CommandBus.PublishAsync(SimpleMapper.Map(request, new AssignContributor())); var command = SimpleMapper.Map(request, new AssignContributor());
var context = await CommandBus.PublishAsync(command);
return NoContent(); var result = context.Result<EntityCreatedResult<string>>();
var response = new ContributorAssignedDto { ContributorId = result.IdOrValue };
return Ok(response);
} }
/// <summary> /// <summary>

1
src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs

@ -99,7 +99,6 @@ namespace Squidex.Areas.Api.Controllers.Apps
public async Task<IActionResult> PostApp([FromBody] CreateAppDto request) public async Task<IActionResult> PostApp([FromBody] CreateAppDto request)
{ {
var command = SimpleMapper.Map(request, new CreateApp()); var command = SimpleMapper.Map(request, new CreateApp());
var context = await CommandBus.PublishAsync(command); var context = await CommandBus.PublishAsync(command);
var result = context.Result<EntityCreatedResult<Guid>>(); var result = context.Result<EntityCreatedResult<Guid>>();

2
src/Squidex/Areas/Api/Controllers/Apps/Models/AssignAppContributorDto.cs

@ -15,7 +15,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
public sealed class AssignAppContributorDto public sealed class AssignAppContributorDto
{ {
/// <summary> /// <summary>
/// The id of the user to add to the app. /// The id or email of the user to add to the app.
/// </summary> /// </summary>
[Required] [Required]
public string ContributorId { get; set; } public string ContributorId { get; set; }

20
src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorAssignedDto.cs

@ -0,0 +1,20 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.ComponentModel.DataAnnotations;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
public sealed class ContributorAssignedDto
{
/// <summary>
/// The id of the user that has been assigned as contributor.
/// </summary>
[Required]
public string ContributorId { get; set; }
}
}

2
src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs

@ -268,7 +268,6 @@ namespace Squidex.Areas.Api.Controllers.Contents
await contentQuery.FindSchemaAsync(App, name); await contentQuery.FindSchemaAsync(App, name);
var command = new UpdateContent { ContentId = id, Data = request.ToCleaned() }; var command = new UpdateContent { ContentId = id, Data = request.ToCleaned() };
var context = await CommandBus.PublishAsync(command); var context = await CommandBus.PublishAsync(command);
var result = context.Result<ContentDataChangedResult>(); var result = context.Result<ContentDataChangedResult>();
@ -301,7 +300,6 @@ namespace Squidex.Areas.Api.Controllers.Contents
await contentQuery.FindSchemaAsync(App, name); await contentQuery.FindSchemaAsync(App, name);
var command = new PatchContent { ContentId = id, Data = request.ToCleaned() }; var command = new PatchContent { ContentId = id, Data = request.ToCleaned() };
var context = await CommandBus.PublishAsync(command); var context = await CommandBus.PublishAsync(command);
var result = context.Result<ContentDataChangedResult>(); var result = context.Result<ContentDataChangedResult>();

9
src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs

@ -11,6 +11,7 @@ using NSwag.Annotations;
using Squidex.Areas.Api.Controllers.Schemas.Models; using Squidex.Areas.Api.Controllers.Schemas.Models;
using Squidex.Domain.Apps.Entities.Schemas.Commands; using Squidex.Domain.Apps.Entities.Schemas.Commands;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Reflection;
using Squidex.Pipeline; using Squidex.Pipeline;
namespace Squidex.Areas.Api.Controllers.Schemas namespace Squidex.Areas.Api.Controllers.Schemas
@ -50,13 +51,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> PostField(string app, string name, [FromBody] AddFieldDto request) public async Task<IActionResult> PostField(string app, string name, [FromBody] AddFieldDto request)
{ {
var command = new AddField var command = SimpleMapper.Map(request, new AddField { Properties = request.Properties.ToProperties() });
{
Name = request.Name,
Partitioning = request.Partitioning,
Properties = request.Properties.ToProperties()
};
var context = await CommandBus.PublishAsync(command); var context = await CommandBus.PublishAsync(command);
var result = context.Result<EntityCreatedResult<long>>(); var result = context.Result<EntityCreatedResult<long>>();

1
src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs

@ -120,7 +120,6 @@ namespace Squidex.Areas.Api.Controllers.Schemas
public async Task<IActionResult> PostSchema(string app, [FromBody] CreateSchemaDto request) public async Task<IActionResult> PostSchema(string app, [FromBody] CreateSchemaDto request)
{ {
var command = request.ToCommand(); var command = request.ToCommand();
var context = await CommandBus.PublishAsync(command); var context = await CommandBus.PublishAsync(command);
var result = context.Result<EntityCreatedResult<Guid>>(); var result = context.Result<EntityCreatedResult<Guid>>();

9
src/Squidex/Areas/Api/Controllers/Users/Models/CreateUserDto.cs

@ -11,13 +11,22 @@ namespace Squidex.Areas.Api.Controllers.Users.Models
{ {
public sealed class CreateUserDto public sealed class CreateUserDto
{ {
/// <summary>
/// The email of the user. Unique value.
/// </summary>
[Required] [Required]
[EmailAddress] [EmailAddress]
public string Email { get; set; } public string Email { get; set; }
/// <summary>
/// The display name (usually first name and last name) of the user.
/// </summary>
[Required] [Required]
public string DisplayName { get; set; } public string DisplayName { get; set; }
/// <summary>
/// The password of the user.
/// </summary>
[Required] [Required]
public string Password { get; set; } public string Password { get; set; }
} }

26
src/Squidex/Areas/Api/Controllers/Users/Models/PublicUserDto.cs

@ -0,0 +1,26 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.ComponentModel.DataAnnotations;
namespace Squidex.Areas.Api.Controllers.Users.Models
{
public sealed class PublicUserDto
{
/// <summary>
/// The id of the user.
/// </summary>
[Required]
public string Id { get; set; }
/// <summary>
/// The display name (usually first name and last name) of the user.
/// </summary>
[Required]
public string DisplayName { get; set; }
}
}

9
src/Squidex/Areas/Api/Controllers/Users/Models/UpdateUserDto.cs

@ -11,13 +11,22 @@ namespace Squidex.Areas.Api.Controllers.Users.Models
{ {
public sealed class UpdateUserDto public sealed class UpdateUserDto
{ {
/// <summary>
/// The email of the user. Unique value.
/// </summary>
[Required] [Required]
[EmailAddress] [EmailAddress]
public string Email { get; set; } public string Email { get; set; }
/// <summary>
/// The display name (usually first name and last name) of the user.
/// </summary>
[Required] [Required]
public string DisplayName { get; set; } public string DisplayName { get; set; }
/// <summary>
/// The password of the user.
/// </summary>
public string Password { get; set; } public string Password { get; set; }
} }
} }

6
src/Squidex/Areas/Api/Controllers/Users/Models/UserCreatedDto.cs

@ -11,10 +11,10 @@ namespace Squidex.Areas.Api.Controllers.Users.Models
{ {
public sealed class UserCreatedDto public sealed class UserCreatedDto
{ {
/// <summary>
/// The id of the user.
/// </summary>
[Required] [Required]
public string Id { get; set; } public string Id { get; set; }
[Required]
public string PictureUrl { get; set; }
} }
} }

6
src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs

@ -23,12 +23,6 @@ namespace Squidex.Areas.Api.Controllers.Users.Models
[Required] [Required]
public string Email { get; set; } public string Email { get; set; }
/// <summary>
/// The url to the profile picture of the user.
/// </summary>
[Required]
public string PictureUrl { get; set; }
/// <summary> /// <summary>
/// The display name (usually first name and last name) of the user. /// The display name (usually first name and last name) of the user.
/// </summary> /// </summary>

4
src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs

@ -82,7 +82,7 @@ namespace Squidex.Areas.Api.Controllers.Users
{ {
var user = await userManager.CreateAsync(userFactory, request.Email, request.DisplayName, request.Password); var user = await userManager.CreateAsync(userFactory, request.Email, request.DisplayName, request.Password);
var response = new UserCreatedDto { Id = user.Id, PictureUrl = user.PictureUrl() }; var response = new UserCreatedDto { Id = user.Id };
return Ok(response); return Ok(response);
} }
@ -129,7 +129,7 @@ namespace Squidex.Areas.Api.Controllers.Users
private static UserDto Map(IUser user) private static UserDto Map(IUser user)
{ {
return SimpleMapper.Map(user, new UserDto { DisplayName = user.DisplayName(), PictureUrl = user.PictureUrl() }); return SimpleMapper.Map(user, new UserDto { DisplayName = user.DisplayName() });
} }
private bool IsSelf(string id) private bool IsSelf(string id)

8
src/Squidex/Areas/Api/Controllers/Users/UsersController.cs

@ -65,12 +65,12 @@ namespace Squidex.Areas.Api.Controllers.Users
[ApiAuthorize] [ApiAuthorize]
[HttpGet] [HttpGet]
[Route("users/")] [Route("users/")]
[ProducesResponseType(typeof(UserDto[]), 200)] [ProducesResponseType(typeof(PublicUserDto[]), 200)]
public async Task<IActionResult> GetUsers(string query) public async Task<IActionResult> GetUsers(string query)
{ {
var entities = await userManager.QueryByEmailAsync(query ?? string.Empty); var entities = await userManager.QueryByEmailAsync(query ?? string.Empty);
var models = entities.Select(x => SimpleMapper.Map(x, new UserDto { DisplayName = x.DisplayName(), PictureUrl = x.PictureUrl() })).ToArray(); var models = entities.Where(x => !x.IsHidden()).Select(x => SimpleMapper.Map(x, new UserDto { DisplayName = x.DisplayName() })).ToArray();
return Ok(models); return Ok(models);
} }
@ -86,7 +86,7 @@ namespace Squidex.Areas.Api.Controllers.Users
[ApiAuthorize] [ApiAuthorize]
[HttpGet] [HttpGet]
[Route("users/{id}/")] [Route("users/{id}/")]
[ProducesResponseType(typeof(UserDto), 200)] [ProducesResponseType(typeof(PublicUserDto), 200)]
public async Task<IActionResult> GetUser(string id) public async Task<IActionResult> GetUser(string id)
{ {
var entity = await userManager.FindByIdAsync(id); var entity = await userManager.FindByIdAsync(id);
@ -96,7 +96,7 @@ namespace Squidex.Areas.Api.Controllers.Users
return NotFound(); return NotFound();
} }
var response = SimpleMapper.Map(entity, new UserDto { DisplayName = entity.DisplayName(), PictureUrl = entity.PictureUrl() }); var response = SimpleMapper.Map(entity, new UserDto { DisplayName = entity.DisplayName() });
return Ok(response); return Ok(response);
} }

2
src/Squidex/Areas/IdentityServer/Controllers/Profile/ChangeProfileModel.cs

@ -17,5 +17,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
[Required(ErrorMessage = "DisplayName is required.")] [Required(ErrorMessage = "DisplayName is required.")]
public string DisplayName { get; set; } public string DisplayName { get; set; }
public bool IsHidden { get; set; }
} }
} }

3
src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs

@ -83,7 +83,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(user => userManager.UpdateAsync(user, model.Email, model.DisplayName), return MakeChangeAsync(user => userManager.UpdateAsync(user, model.Email, model.DisplayName, model.IsHidden),
"Account updated successfully."); "Account updated successfully.");
} }
@ -195,6 +195,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
ExternalLogins = user.Logins, ExternalLogins = user.Logins,
ExternalProviders = externalProviders, ExternalProviders = externalProviders,
DisplayName = user.DisplayName(), DisplayName = user.DisplayName(),
IsHidden = user.IsHidden(),
HasPassword = await userManager.HasPasswordAsync(user), HasPassword = await userManager.HasPasswordAsync(user),
HasPasswordAuth = identityOptions.Value.AllowPasswordAuth, HasPasswordAuth = identityOptions.Value.AllowPasswordAuth,
SuccessMessage = successMessage SuccessMessage = successMessage

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

@ -22,6 +22,8 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
public string SuccessMessage { get; set; } public string SuccessMessage { get; set; }
public bool IsHidden { get; set; }
public bool HasPassword { get; set; } public bool HasPassword { get; set; }
public bool HasPasswordAuth { get; set; } public bool HasPasswordAuth { get; set; }

12
src/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml

@ -52,7 +52,7 @@
</div> </div>
} }
<input type="email" ap class="form-control" asp-for="Email" name="email" id="email" /> <input type="email" ap class="form-control" asp-for="Email" id="email" />
</div> </div>
<div class="form-group"> <div class="form-group">
@ -65,7 +65,15 @@
</div> </div>
} }
<input type="text" class="form-control" asp-for="DisplayName" name="displayName" id="displayName"/> <input type="text" class="form-control" asp-for="DisplayName" id="displayName"/>
</div>
<div class="form-group">
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="IsHidden" id="hidden" />
<label class="form-check-label" for="hidden">Do not show my profile to other users</label>
</div>
</div> </div>
<button type="submit" class="btn btn-primary">Save</button> <button type="submit" class="btn btn-primary">Save</button>

1
src/Squidex/app/features/administration/pages/users/user-page.component.ts

@ -68,7 +68,6 @@ export class UserPageComponent implements OnInit {
created.id, created.id,
requestDto.email, requestDto.email,
requestDto.displayName, requestDto.displayName,
created.pictureUrl!,
false); false);
this.ctx.notifyInfo('User created successfully.'); this.ctx.notifyInfo('User created successfully.');

14
src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html

@ -27,16 +27,13 @@
<td class="cell-auto"> <td class="cell-auto">
<span class="user-name table-cell">{{contributor.contributorId | sqxUserName}}</span> <span class="user-name table-cell">{{contributor.contributorId | sqxUserName}}</span>
</td> </td>
<td class="cell-auto">
<span class="user-email table-cell">{{contributor.contributorId | sqxUserEmail}}</span>
</td>
<td class="cell-time"> <td class="cell-time">
<select class="form-control" [ngModel]="contributor.permission" (ngModelChange)="changePermission(contributor, $event)" [disabled]="userId === contributor.contributorId"> <select class="form-control" [ngModel]="contributor.permission" (ngModelChange)="changePermission(contributor, $event)" [disabled]="userId === contributor.contributorId">
<option *ngFor="let permission of usersPermissions" [ngValue]="permission">{{permission}}</option> <option *ngFor="let permission of usersPermissions" [ngValue]="permission">{{permission}}</option>
</select> </select>
</td> </td>
<td class="cell-actions"> <td class="cell-actions">
<button type="button" class="btn btn-link btn-danger" [disabled]="userId === contributor.contributorId" (click)="removeContributor(contributor)"> <button *ngIf="ctx.userId !== contributor.contributorId" type="button" class="btn btn-link btn-danger" (click)="removeContributor(contributor)">
<i class="icon-bin2"></i> <i class="icon-bin2"></i>
</button> </button>
</td> </td>
@ -50,12 +47,13 @@
<form [formGroup]="addContributorForm" (ngSubmit)="assignContributor()"> <form [formGroup]="addContributorForm" (ngSubmit)="assignContributor()">
<div class="row no-gutters"> <div class="row no-gutters">
<div class="col"> <div class="col">
<sqx-autocomplete [source]="usersDataSource" formControlName="user" [inputName]="'contributor'" placeholder="Find existing user" displayProperty="email"> <sqx-autocomplete [source]="usersDataSource" formControlName="user" [inputName]="'contributor'" placeholder="Find existing user" displayProperty="displayName">
<ng-template let-user="$implicit"> <ng-template let-user="$implicit">
<img class="user-picture autocomplete-user-picture" [attr.src]="user | sqxUserDtoPicture" /> <span class="autocomplete-user">
<img class="user-picture autocomplete-user-picture" [attr.src]="user | sqxUserDtoPicture" />
<span class="user-name autocomplete-user-name">{{user.displayName}}</span> <span class="user-name autocomplete-user-name">{{user.displayName}}</span>
<span class="user-email autocomplete-user-email">{{user.email}}</span> </span>
</ng-template> </ng-template>
</sqx-autocomplete> </sqx-autocomplete>
</div> </div>

11
src/Squidex/app/features/settings/pages/contributors/contributors-page.component.scss

@ -2,14 +2,11 @@
@import '_mixins'; @import '_mixins';
.autocomplete-user { .autocomplete-user {
&-picture { & {
float: left; @include truncate;
margin-top: .4rem;
} }
&-name, &-name {
&-email { margin-left: .25rem;
@include truncate;
margin-left: 3rem;
} }
} }

13
src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts

@ -16,6 +16,7 @@ import {
AppContributorsService, AppContributorsService,
AutocompleteSource, AutocompleteSource,
HistoryChannelUpdated, HistoryChannelUpdated,
PublicUserDto,
UsersService UsersService
} from 'shared'; } from 'shared';
@ -110,16 +111,20 @@ export class ContributorsPageComponent implements OnInit {
} }
public assignContributor() { public assignContributor() {
const requestDto = new AppContributorDto(this.addContributorForm.controls['user'].value.id, 'Editor'); let value: any = this.addContributorForm.controls['user'].value;
if (value instanceof PublicUserDto) {
value = value.id;
}
const requestDto = new AppContributorDto(value, 'Editor');
this.appContributorsService.postContributor(this.ctx.appName, requestDto, this.appContributors.version) this.appContributorsService.postContributor(this.ctx.appName, requestDto, this.appContributors.version)
.subscribe(dto => { .subscribe(dto => {
this.updateContributors(this.appContributors.addContributor(requestDto, dto.version)); this.updateContributors(this.appContributors.addContributor(new AppContributorDto(dto.payload.contributorId, requestDto.permission), dto.version));
this.resetContributorForm(); this.resetContributorForm();
}, error => { }, error => {
this.ctx.notifyError(error); this.ctx.notifyError(error);
this.resetContributorForm();
}); });
} }

7
src/Squidex/app/framework/angular/autocomplete.component.ts

@ -61,15 +61,18 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O
public ngOnInit() { public ngOnInit() {
this.subscription = this.subscription =
this.queryInput.valueChanges this.queryInput.valueChanges
.do(query => {
this.callChange(query);
})
.map(query => <string>query) .map(query => <string>query)
.map(query => query ? query.trim() : query) .map(query => query ? query.trim() : query)
.distinctUntilChanged()
.debounceTime(200)
.do(query => { .do(query => {
if (!query) { if (!query) {
this.reset(); this.reset();
} }
}) })
.distinctUntilChanged()
.debounceTime(200)
.filter(query => !!query && !!this.source) .filter(query => !!query && !!this.source)
.switchMap(query => this.source.find(query)).catch(error => Observable.of([])) .switchMap(query => this.source.find(query)).catch(error => Observable.of([]))
.subscribe(items => { .subscribe(items => {

40
src/Squidex/app/shared/components/pipes.ts

@ -10,7 +10,7 @@ import { Observable, Subscription } from 'rxjs';
import { ApiUrlConfig, MathHelper } from 'framework'; import { ApiUrlConfig, MathHelper } from 'framework';
import { UserDto, UsersProviderService } from './../declarations-base'; import { PublicUserDto, UsersProviderService } from './../declarations-base';
class UserAsyncPipe implements OnDestroy { class UserAsyncPipe implements OnDestroy {
private lastUserId: string; private lastUserId: string;
@ -88,42 +88,6 @@ export class UserNameRefPipe extends UserAsyncPipe implements PipeTransform {
} }
} }
@Pipe({
name: 'sqxUserEmail',
pure: false
})
export class UserEmailPipe extends UserAsyncPipe implements PipeTransform {
constructor(users: UsersProviderService, changeDetector: ChangeDetectorRef) {
super(users, changeDetector);
}
public transform(userId: string): string | null {
return super.transformInternal(userId, users => users.getUser(userId).map(u => u.email));
}
}
@Pipe({
name: 'sqxUserEmailRef',
pure: false
})
export class UserEmailRefPipe extends UserAsyncPipe implements PipeTransform {
constructor(users: UsersProviderService, changeDetector: ChangeDetectorRef) {
super(users, changeDetector);
}
public transform(userId: string): string | null {
return super.transformInternal(userId, users => {
const parts = userId.split(':');
if (parts[0] === 'subject') {
return users.getUser(parts[1]).map(u => u.email);
} else {
return Observable.of(null);
}
});
}
}
@Pipe({ @Pipe({
name: 'sqxUserDtoPicture', name: 'sqxUserDtoPicture',
pure: false pure: false
@ -134,7 +98,7 @@ export class UserDtoPicture implements PipeTransform {
) { ) {
} }
public transform(user: UserDto): string | null { public transform(user: PublicUserDto): string | null {
return this.apiUrl.buildUrl(`api/users/${user.id}/picture`); return this.apiUrl.buildUrl(`api/users/${user.id}/picture`);
} }
} }

6
src/Squidex/app/shared/module.ts

@ -53,8 +53,6 @@ import {
UnsetAppGuard, UnsetAppGuard,
UsagesService, UsagesService,
UserDtoPicture, UserDtoPicture,
UserEmailPipe,
UserEmailRefPipe,
UserNamePipe, UserNamePipe,
UserNameRefPipe, UserNameRefPipe,
UserIdPicturePipe, UserIdPicturePipe,
@ -83,8 +81,6 @@ import {
LanguageSelectorComponent, LanguageSelectorComponent,
MarkdownEditorComponent, MarkdownEditorComponent,
UserDtoPicture, UserDtoPicture,
UserEmailPipe,
UserEmailRefPipe,
UserIdPicturePipe, UserIdPicturePipe,
UserNamePipe, UserNamePipe,
UserNameRefPipe, UserNameRefPipe,
@ -104,8 +100,6 @@ import {
LanguageSelectorComponent, LanguageSelectorComponent,
MarkdownEditorComponent, MarkdownEditorComponent,
UserDtoPicture, UserDtoPicture,
UserEmailPipe,
UserEmailRefPipe,
UserIdPicturePipe, UserIdPicturePipe,
UserNamePipe, UserNamePipe,
UserNameRefPipe, UserNameRefPipe,

13
src/Squidex/app/shared/services/app-contributors.service.spec.ts

@ -14,7 +14,8 @@ import {
AppContributorDto, AppContributorDto,
AppContributorsDto, AppContributorsDto,
AppContributorsService, AppContributorsService,
Version Version,
ContributorAssignedDto
} from './../'; } from './../';
describe('AppContributorsDto', () => { describe('AppContributorsDto', () => {
@ -122,14 +123,20 @@ describe('AppContributorsService', () => {
const dto = new AppContributorDto('123', 'Owner'); const dto = new AppContributorDto('123', 'Owner');
appContributorsService.postContributor('my-app', dto, version).subscribe(); let contributorAssignedDto: ContributorAssignedDto | null = null;
appContributorsService.postContributor('my-app', dto, version).subscribe(result => {
contributorAssignedDto = result.payload;
});
const req = httpMock.expectOne('http://service/p/api/apps/my-app/contributors'); const req = httpMock.expectOne('http://service/p/api/apps/my-app/contributors');
expect(req.request.method).toEqual('POST'); expect(req.request.method).toEqual('POST');
expect(req.request.headers.get('If-Match')).toEqual(version.value); expect(req.request.headers.get('If-Match')).toEqual(version.value);
req.flush({}); req.flush({ contributorId: '123' });
expect(contributorAssignedDto!.contributorId).toEqual('123');
})); }));
it('should make delete request to remove contributor', it('should make delete request to remove contributor',

16
src/Squidex/app/shared/services/app-contributors.service.ts

@ -58,6 +58,13 @@ export class AppContributorDto {
} }
} }
export class ContributorAssignedDto {
constructor(
public readonly contributorId: string
) {
}
}
@Injectable() @Injectable()
export class AppContributorsService { export class AppContributorsService {
constructor( constructor(
@ -87,10 +94,17 @@ export class AppContributorsService {
.pretifyError('Failed to load contributors. Please reload.'); .pretifyError('Failed to load contributors. Please reload.');
} }
public postContributor(appName: string, dto: AppContributorDto, version: Version): Observable<Versioned<any>> { public postContributor(appName: string, dto: AppContributorDto, version: Version): Observable<Versioned<ContributorAssignedDto>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/contributors`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/contributors`);
return HTTP.postVersioned(this.http, url, dto, version) return HTTP.postVersioned(this.http, url, dto, version)
.map(response => {
const body: any = response.payload.body;
const result = new ContributorAssignedDto(body.contributorId);
return new Versioned(response.version, result);
})
.do(() => { .do(() => {
this.analytics.trackEvent('Contributor', 'Configured', appName); this.analytics.trackEvent('Contributor', 'Configured', appName);
}) })

20
src/Squidex/app/shared/services/users-provider.service.spec.ts

@ -11,7 +11,7 @@ import { IMock, Mock, Times } from 'typemoq';
import { import {
AuthService, AuthService,
Profile, Profile,
UserDto, PublicUserDto,
UsersProviderService, UsersProviderService,
UsersService UsersService
} from './../'; } from './../';
@ -28,12 +28,12 @@ describe('UsersProviderService', () => {
}); });
it('should return users service when user not cached', () => { it('should return users service when user not cached', () => {
const user = new UserDto('123', 'mail@domain.com', 'User1', 'path/to/image', true); const user = new PublicUserDto('123', 'User1');
usersService.setup(x => x.getUser('123')) usersService.setup(x => x.getUser('123'))
.returns(() => Observable.of(user)).verifiable(Times.once()); .returns(() => Observable.of(user)).verifiable(Times.once());
let resultingUser: UserDto | null = null; let resultingUser: PublicUserDto | null = null;
usersProviderService.getUser('123').subscribe(result => { usersProviderService.getUser('123').subscribe(result => {
resultingUser = result; resultingUser = result;
@ -45,14 +45,14 @@ describe('UsersProviderService', () => {
}); });
it('should return provide user from cache', () => { it('should return provide user from cache', () => {
const user = new UserDto('123', 'mail@domain.com', 'User1', 'path/to/image', true); const user = new PublicUserDto('123', 'User1');
usersService.setup(x => x.getUser('123')) usersService.setup(x => x.getUser('123'))
.returns(() => Observable.of(user)).verifiable(Times.once()); .returns(() => Observable.of(user)).verifiable(Times.once());
usersProviderService.getUser('123'); usersProviderService.getUser('123');
let resultingUser: UserDto | null = null; let resultingUser: PublicUserDto | null = null;
usersProviderService.getUser('123').subscribe(result => { usersProviderService.getUser('123').subscribe(result => {
resultingUser = result; resultingUser = result;
@ -64,7 +64,7 @@ describe('UsersProviderService', () => {
}); });
it('should return me when user is current user', () => { it('should return me when user is current user', () => {
const user = new UserDto('123', 'mail@domain.com', 'User1', 'path/to/image', true); const user = new PublicUserDto('123', 'User1');
authService.setup(x => x.user) authService.setup(x => x.user)
.returns(() => new Profile(<any>{ profile: { sub: '123'}})); .returns(() => new Profile(<any>{ profile: { sub: '123'}}));
@ -72,13 +72,13 @@ describe('UsersProviderService', () => {
usersService.setup(x => x.getUser('123')) usersService.setup(x => x.getUser('123'))
.returns(() => Observable.of(user)).verifiable(Times.once()); .returns(() => Observable.of(user)).verifiable(Times.once());
let resultingUser: UserDto | null = null; let resultingUser: PublicUserDto | null = null;
usersProviderService.getUser('123').subscribe(result => { usersProviderService.getUser('123').subscribe(result => {
resultingUser = result; resultingUser = result;
}).unsubscribe(); }).unsubscribe();
expect(resultingUser).toEqual(new UserDto('123', 'mail@domain.com', 'Me', 'path/to/image', true)); expect(resultingUser).toEqual(new PublicUserDto('123', 'Me'));
usersService.verifyAll(); usersService.verifyAll();
}); });
@ -90,13 +90,13 @@ describe('UsersProviderService', () => {
usersService.setup(x => x.getUser('123')) usersService.setup(x => x.getUser('123'))
.returns(() => Observable.throw('NOT FOUND')).verifiable(Times.once()); .returns(() => Observable.throw('NOT FOUND')).verifiable(Times.once());
let resultingUser: UserDto | null = null; let resultingUser: PublicUserDto | null = null;
usersProviderService.getUser('123').subscribe(result => { usersProviderService.getUser('123').subscribe(result => {
resultingUser = result; resultingUser = result;
}).unsubscribe(); }).unsubscribe();
expect(resultingUser).toEqual(new UserDto('NOT FOUND', 'NOT FOUND', 'NOT FOUND', null, false)); expect(resultingUser).toEqual(new PublicUserDto('unknown', 'unknown'));
usersService.verifyAll(); usersService.verifyAll();
}); });

10
src/Squidex/app/shared/services/users-provider.service.ts

@ -8,13 +8,13 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { UserDto, UsersService } from './users.service'; import { PublicUserDto, UsersService } from './users.service';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
@Injectable() @Injectable()
export class UsersProviderService { export class UsersProviderService {
private readonly caches: { [id: string]: Observable<UserDto> } = {}; private readonly caches: { [id: string]: Observable<PublicUserDto> } = {};
constructor( constructor(
private readonly usersService: UsersService, private readonly usersService: UsersService,
@ -22,14 +22,14 @@ export class UsersProviderService {
) { ) {
} }
public getUser(id: string, me: string | null = 'Me'): Observable<UserDto> { public getUser(id: string, me: string | null = 'Me'): Observable<PublicUserDto> {
let result = this.caches[id]; let result = this.caches[id];
if (!result) { if (!result) {
const request = const request =
this.usersService.getUser(id) this.usersService.getUser(id)
.catch(error => { .catch(error => {
return Observable.of(new UserDto('NOT FOUND', 'NOT FOUND', 'NOT FOUND', null, false)); return Observable.of(new PublicUserDto('Unknown', 'Unknown'));
}) })
.publishLast(); .publishLast();
@ -41,7 +41,7 @@ export class UsersProviderService {
return result return result
.map(dto => { .map(dto => {
if (me && this.authService.user && dto.id === this.authService.user.id) { if (me && this.authService.user && dto.id === this.authService.user.id) {
dto = new UserDto(dto.id, dto.email, me, dto.pictureUrl, dto.isLocked); dto = new PublicUserDto(dto.id, me);
} }
return dto; return dto;
}).share(); }).share();

68
src/Squidex/app/shared/services/users.service.spec.ts

@ -11,6 +11,7 @@ import { inject, TestBed } from '@angular/core/testing';
import { import {
ApiUrlConfig, ApiUrlConfig,
CreateUserDto, CreateUserDto,
PublicUserDto,
UpdateUserDto, UpdateUserDto,
UserDto, UserDto,
UserManagementService, UserManagementService,
@ -20,7 +21,7 @@ import {
describe('UserDto', () => { describe('UserDto', () => {
it('should update email and display name property when unlocking', () => { it('should update email and display name property when unlocking', () => {
const user_1 = new UserDto('1', 'sebastian@squidex.io', 'Sebastian', 'picture', true); const user_1 = new UserDto('1', 'sebastian@squidex.io', 'Sebastian', true);
const user_2 = user_1.update('qaisar@squidex.io', 'Qaisar'); const user_2 = user_1.update('qaisar@squidex.io', 'Qaisar');
expect(user_2.email).toEqual('qaisar@squidex.io'); expect(user_2.email).toEqual('qaisar@squidex.io');
@ -28,14 +29,14 @@ describe('UserDto', () => {
}); });
it('should update isLocked property when locking', () => { it('should update isLocked property when locking', () => {
const user_1 = new UserDto('1', 'sebastian@squidex.io', 'Sebastian', 'picture', false); const user_1 = new UserDto('1', 'sebastian@squidex.io', 'Sebastian', false);
const user_2 = user_1.lock(); const user_2 = user_1.lock();
expect(user_2.isLocked).toBeTruthy(); expect(user_2.isLocked).toBeTruthy();
}); });
it('should update isLocked property when unlocking', () => { it('should update isLocked property when unlocking', () => {
const user_1 = new UserDto('1', 'sebastian@squidex.io', 'Sebastian', 'picture', true); const user_1 = new UserDto('1', 'sebastian@squidex.io', 'Sebastian', true);
const user_2 = user_1.unlock(); const user_2 = user_1.unlock();
expect(user_2.isLocked).toBeFalsy(); expect(user_2.isLocked).toBeFalsy();
@ -62,7 +63,7 @@ describe('UsersService', () => {
it('should make get request to get many users', it('should make get request to get many users',
inject([UsersService, HttpTestingController], (usersService: UsersService, httpMock: HttpTestingController) => { inject([UsersService, HttpTestingController], (usersService: UsersService, httpMock: HttpTestingController) => {
let users: UserDto[] | null = null; let users: PublicUserDto[] | null = null;
usersService.getUsers().subscribe(result => { usersService.getUsers().subscribe(result => {
users = result; users = result;
@ -76,31 +77,25 @@ describe('UsersService', () => {
req.flush([ req.flush([
{ {
id: '123', id: '123',
email: 'mail1@domain.com', displayName: 'User1'
displayName: 'User1',
pictureUrl: 'path/to/image1',
isLocked: true
}, },
{ {
id: '456', id: '456',
email: 'mail2@domain.com', displayName: 'User2'
displayName: 'User2',
pictureUrl: 'path/to/image2',
isLocked: true
} }
]); ]);
expect(users).toEqual( expect(users).toEqual(
[ [
new UserDto('123', 'mail1@domain.com', 'User1', 'path/to/image1', true), new UserDto('123', 'mail1@domain.com', 'User1', true),
new UserDto('456', 'mail2@domain.com', 'User2', 'path/to/image2', true) new UserDto('456', 'mail2@domain.com', 'User2', true)
]); ]);
})); }));
it('should make get request with query to get many users', it('should make get request with query to get many users',
inject([UsersService, HttpTestingController], (usersService: UsersService, httpMock: HttpTestingController) => { inject([UsersService, HttpTestingController], (usersService: UsersService, httpMock: HttpTestingController) => {
let users: UserDto[] | null = null; let users: PublicUserDto[] | null = null;
usersService.getUsers('my-query').subscribe(result => { usersService.getUsers('my-query').subscribe(result => {
users = result; users = result;
@ -114,31 +109,25 @@ describe('UsersService', () => {
req.flush([ req.flush([
{ {
id: '123', id: '123',
email: 'mail1@domain.com', displayName: 'User1'
displayName: 'User1',
pictureUrl: 'path/to/image1',
isLocked: true
}, },
{ {
id: '456', id: '456',
email: 'mail2@domain.com', displayName: 'User2'
displayName: 'User2',
pictureUrl: 'path/to/image2',
isLocked: true
} }
]); ]);
expect(users).toEqual( expect(users).toEqual(
[ [
new UserDto('123', 'mail1@domain.com', 'User1', 'path/to/image1', true), new PublicUserDto('123', 'User1'),
new UserDto('456', 'mail2@domain.com', 'User2', 'path/to/image2', true) new PublicUserDto('456', 'User2')
]); ]);
})); }));
it('should make get request to get single user', it('should make get request to get single user',
inject([UsersService, HttpTestingController], (usersService: UsersService, httpMock: HttpTestingController) => { inject([UsersService, HttpTestingController], (usersService: UsersService, httpMock: HttpTestingController) => {
let user: UserDto | null = null; let user: PublicUserDto | null = null;
usersService.getUser('123').subscribe(result => { usersService.getUser('123').subscribe(result => {
user = result; user = result;
@ -149,15 +138,9 @@ describe('UsersService', () => {
expect(req.request.method).toEqual('GET'); expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull(); expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({ req.flush({ id: '123', displayName: 'User1' });
id: '123',
email: 'mail1@domain.com',
displayName: 'User1',
pictureUrl: 'path/to/image1',
isLocked: true
});
expect(user).toEqual(new UserDto('123', 'mail1@domain.com', 'User1', 'path/to/image1', true)); expect(user).toEqual(new PublicUserDto('123', 'User1'));
})); }));
}); });
@ -199,14 +182,12 @@ describe('UserManagementService', () => {
id: '123', id: '123',
email: 'mail1@domain.com', email: 'mail1@domain.com',
displayName: 'User1', displayName: 'User1',
pictureUrl: 'path/to/image1',
isLocked: true isLocked: true
}, },
{ {
id: '456', id: '456',
email: 'mail2@domain.com', email: 'mail2@domain.com',
displayName: 'User2', displayName: 'User2',
pictureUrl: 'path/to/image2',
isLocked: true isLocked: true
} }
] ]
@ -214,8 +195,8 @@ describe('UserManagementService', () => {
expect(users).toEqual( expect(users).toEqual(
new UsersDto(100, [ new UsersDto(100, [
new UserDto('123', 'mail1@domain.com', 'User1', 'path/to/image1', true), new UserDto('123', 'mail1@domain.com', 'User1', true),
new UserDto('456', 'mail2@domain.com', 'User2', 'path/to/image2', true) new UserDto('456', 'mail2@domain.com', 'User2', true)
])); ]));
})); }));
@ -255,8 +236,8 @@ describe('UserManagementService', () => {
expect(users).toEqual( expect(users).toEqual(
new UsersDto(100, [ new UsersDto(100, [
new UserDto('123', 'mail1@domain.com', 'User1', 'path/to/image1', true), new UserDto('123', 'mail1@domain.com', 'User1', true),
new UserDto('456', 'mail2@domain.com', 'User2', 'path/to/image2', true) new UserDto('456', 'mail2@domain.com', 'User2', true)
])); ]));
})); }));
@ -278,11 +259,10 @@ describe('UserManagementService', () => {
id: '123', id: '123',
email: 'mail1@domain.com', email: 'mail1@domain.com',
displayName: 'User1', displayName: 'User1',
pictureUrl: 'path/to/image1',
isLocked: true isLocked: true
}); });
expect(user).toEqual(new UserDto('123', 'mail1@domain.com', 'User1', 'path/to/image1', true)); expect(user).toEqual(new UserDto('123', 'mail1@domain.com', 'User1', true));
})); }));
it('should make post request to create user', it('should make post request to create user',
@ -301,9 +281,9 @@ describe('UserManagementService', () => {
expect(req.request.method).toEqual('POST'); expect(req.request.method).toEqual('POST');
expect(req.request.headers.get('If-Match')).toBeNull(); expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({ id: '123', pictureUrl: 'path/to/image1' }); req.flush({ id: '123' });
expect(user).toEqual(new UserDto('123', dto.email, dto.displayName, 'path/to/image1', false)); expect(user).toEqual(new UserDto('123', dto.email, dto.displayName, false));
})); }));
it('should make put request to update user', it('should make put request to update user',

37
src/Squidex/app/shared/services/users.service.ts

@ -26,21 +26,20 @@ export class UserDto {
public readonly id: string, public readonly id: string,
public readonly email: string, public readonly email: string,
public readonly displayName: string, public readonly displayName: string,
public readonly pictureUrl: string | null,
public readonly isLocked: boolean public readonly isLocked: boolean
) { ) {
} }
public update(email: string, displayName: string): UserDto { public update(email: string, displayName: string): UserDto {
return new UserDto(this.id, email, displayName, this.pictureUrl, this.isLocked); return new UserDto(this.id, email, displayName, this.isLocked);
} }
public lock(): UserDto { public lock(): UserDto {
return new UserDto(this.id, this.email, this.displayName, this.pictureUrl, true); return new UserDto(this.id, this.email, this.displayName, true);
} }
public unlock(): UserDto { public unlock(): UserDto {
return new UserDto(this.id, this.email, this.displayName, this.pictureUrl, false); return new UserDto(this.id, this.email, this.displayName, false);
} }
} }
@ -62,6 +61,14 @@ export class UpdateUserDto {
} }
} }
export class PublicUserDto {
constructor(
public readonly id: string,
public readonly displayName: string
) {
}
}
@Injectable() @Injectable()
export class UsersService { export class UsersService {
constructor( constructor(
@ -70,7 +77,7 @@ export class UsersService {
) { ) {
} }
public getUsers(query?: string): Observable<UserDto[]> { public getUsers(query?: string): Observable<PublicUserDto[]> {
const url = this.apiUrl.buildUrl(`api/users?query=${query || ''}`); const url = this.apiUrl.buildUrl(`api/users?query=${query || ''}`);
return HTTP.getVersioned<any>(this.http, url) return HTTP.getVersioned<any>(this.http, url)
@ -80,30 +87,24 @@ export class UsersService {
const items: any[] = body; const items: any[] = body;
return items.map(item => { return items.map(item => {
return new UserDto( return new PublicUserDto(
item.id, item.id,
item.email, item.displayName);
item.displayName,
item.pictureUrl,
item.isLocked);
}); });
}) })
.pretifyError('Failed to load users. Please reload.'); .pretifyError('Failed to load users. Please reload.');
} }
public getUser(id: string): Observable<UserDto> { public getUser(id: string): Observable<PublicUserDto> {
const url = this.apiUrl.buildUrl(`api/users/${id}`); const url = this.apiUrl.buildUrl(`api/users/${id}`);
return HTTP.getVersioned<any>(this.http, url) return HTTP.getVersioned<any>(this.http, url)
.map(response => { .map(response => {
const body = response.payload.body; const body = response.payload.body;
return new UserDto( return new PublicUserDto(
body.id, body.id,
body.email, body.displayName);
body.displayName,
body.pictureUrl,
body.isLocked);
}) })
.pretifyError('Failed to load user. Please reload.'); .pretifyError('Failed to load user. Please reload.');
} }
@ -131,7 +132,6 @@ export class UserManagementService {
item.id, item.id,
item.email, item.email,
item.displayName, item.displayName,
item.pictureUrl,
item.isLocked); item.isLocked);
}); });
@ -151,7 +151,6 @@ export class UserManagementService {
body.id, body.id,
body.email, body.email,
body.displayName, body.displayName,
body.pictureUrl,
body.isLocked); body.isLocked);
}) })
.pretifyError('Failed to load user. Please reload.'); .pretifyError('Failed to load user. Please reload.');
@ -164,7 +163,7 @@ export class UserManagementService {
.map(response => { .map(response => {
const body = response.payload.body; const body = response.payload.body;
return new UserDto(body.id, dto.email, dto.displayName, body.pictureUrl, false); return new UserDto(body.id, dto.email, dto.displayName, false);
}) })
.pretifyError('Failed to create user. Please reload.'); .pretifyError('Failed to create user. Please reload.');
} }

12
tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs

@ -27,6 +27,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); private readonly IAppProvider appProvider = A.Fake<IAppProvider>();
private readonly IAppPlansProvider appPlansProvider = A.Fake<IAppPlansProvider>(); private readonly IAppPlansProvider appPlansProvider = A.Fake<IAppPlansProvider>();
private readonly IAppPlanBillingManager appPlansBillingManager = A.Fake<IAppPlanBillingManager>(); private readonly IAppPlanBillingManager appPlansBillingManager = A.Fake<IAppPlanBillingManager>();
private readonly IUser user = A.Fake<IUser>();
private readonly IUserResolver userResolver = A.Fake<IUserResolver>(); private readonly IUserResolver userResolver = A.Fake<IUserResolver>();
private readonly string contributorId = Guid.NewGuid().ToString(); private readonly string contributorId = Guid.NewGuid().ToString();
private readonly string clientId = "client"; private readonly string clientId = "client";
@ -46,10 +47,13 @@ namespace Squidex.Domain.Apps.Entities.Apps
public AppGrainTests() public AppGrainTests()
{ {
A.CallTo(() => appProvider.GetAppAsync(AppName)) A.CallTo(() => appProvider.GetAppAsync(AppName))
.Returns((IAppEntity)null); .Returns((IAppEntity)null);
A.CallTo(() => userResolver.FindByIdAsync(contributorId)) A.CallTo(() => user.Id)
.Returns(A.Fake<IUser>()); .Returns(contributorId);
A.CallTo(() => userResolver.FindByIdOrEmailAsync(contributorId))
.Returns(user);
initialPatterns = new InitialPatterns initialPatterns = new InitialPatterns
{ {
@ -163,7 +167,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
var result = await sut.ExecuteAsync(CreateCommand(command)); var result = await sut.ExecuteAsync(CreateCommand(command));
result.ShouldBeEquivalent(new EntitySavedResult(5)); result.ShouldBeEquivalent(EntityCreatedResult.Create(contributorId, 5));
Assert.Equal(AppContributorPermission.Editor, sut.Snapshot.Contributors[contributorId]); Assert.Equal(AppContributorPermission.Editor, sut.Snapshot.Contributors[contributorId]);

34
tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppContributorsTests.cs

@ -20,14 +20,29 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
{ {
public class GuardAppContributorsTests public class GuardAppContributorsTests
{ {
private readonly IUser user1 = A.Fake<IUser>();
private readonly IUser user2 = A.Fake<IUser>();
private readonly IUser user3 = A.Fake<IUser>();
private readonly IUserResolver users = A.Fake<IUserResolver>(); private readonly IUserResolver users = A.Fake<IUserResolver>();
private readonly IAppLimitsPlan appPlan = A.Fake<IAppLimitsPlan>(); private readonly IAppLimitsPlan appPlan = A.Fake<IAppLimitsPlan>();
private readonly AppContributors contributors_0 = AppContributors.Empty; private readonly AppContributors contributors_0 = AppContributors.Empty;
public GuardAppContributorsTests() public GuardAppContributorsTests()
{ {
A.CallTo(() => users.FindByIdAsync(A<string>.Ignored)) A.CallTo(() => user1.Id).Returns("1");
.Returns(A.Fake<IUser>()); A.CallTo(() => user2.Id).Returns("2");
A.CallTo(() => user3.Id).Returns("3");
A.CallTo(() => users.FindByIdOrEmailAsync("1")).Returns(user1);
A.CallTo(() => users.FindByIdOrEmailAsync("2")).Returns(user2);
A.CallTo(() => users.FindByIdOrEmailAsync("3")).Returns(user3);
A.CallTo(() => users.FindByIdOrEmailAsync("1@email.com")).Returns(user1);
A.CallTo(() => users.FindByIdOrEmailAsync("2@email.com")).Returns(user2);
A.CallTo(() => users.FindByIdOrEmailAsync("3@email.com")).Returns(user3);
A.CallTo(() => users.FindByIdOrEmailAsync("notfound"))
.Returns(Task.FromResult<IUser>(null));
A.CallTo(() => appPlan.MaxContributors) A.CallTo(() => appPlan.MaxContributors)
.Returns(10); .Returns(10);
@ -62,10 +77,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
[Fact] [Fact]
public Task CanAssign_should_throw_exception_if_user_not_found() public Task CanAssign_should_throw_exception_if_user_not_found()
{ {
A.CallTo(() => users.FindByIdAsync(A<string>.Ignored)) var command = new AssignContributor { ContributorId = "notfound", Permission = (AppContributorPermission)10 };
.Returns(Task.FromResult<IUser>(null));
var command = new AssignContributor { ContributorId = "1", Permission = (AppContributorPermission)10 };
return Assert.ThrowsAsync<ValidationException>(() => GuardAppContributors.CanAssign(contributors_0, command, users, appPlan)); return Assert.ThrowsAsync<ValidationException>(() => GuardAppContributors.CanAssign(contributors_0, command, users, appPlan));
} }
@ -84,6 +96,16 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
return Assert.ThrowsAsync<ValidationException>(() => GuardAppContributors.CanAssign(contributors_2, command, users, appPlan)); return Assert.ThrowsAsync<ValidationException>(() => GuardAppContributors.CanAssign(contributors_2, command, users, appPlan));
} }
[Fact]
public async Task CanAssign_assign_if_if_user_added_by_email()
{
var command = new AssignContributor { ContributorId = "1@email.com" };
await GuardAppContributors.CanAssign(contributors_0, command, users, appPlan);
Assert.Equal("1", command.ContributorId);
}
[Fact] [Fact]
public Task CanAssign_should_not_throw_exception_if_user_found() public Task CanAssign_should_not_throw_exception_if_user_found()
{ {

2
tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppTests.cs

@ -27,7 +27,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
A.CallTo(() => apps.GetAppAsync("new-app")) A.CallTo(() => apps.GetAppAsync("new-app"))
.Returns(Task.FromResult<IAppEntity>(null)); .Returns(Task.FromResult<IAppEntity>(null));
A.CallTo(() => users.FindByIdAsync(A<string>.Ignored)) A.CallTo(() => users.FindByIdOrEmailAsync(A<string>.Ignored))
.Returns(A.Fake<IUser>()); .Returns(A.Fake<IUser>());
A.CallTo(() => appPlans.GetPlan("free")) A.CallTo(() => appPlans.GetPlan("free"))

Loading…
Cancel
Save