Browse Source

Merge branch 'master' into ui-logic-rework

# Conflicts:
#	src/Squidex/app/features/administration/pages/users/user-page.component.ts
#	src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html
#	src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts
#	src/Squidex/app/features/settings/pages/plans/plans-page.component.html
#	src/Squidex/app/shared/components/pipes.ts
#	src/Squidex/app/shared/services/users.service.spec.ts
#	src/Squidex/app/shared/services/users.service.ts
pull/271/head
Sebastian Stehle 8 years ago
parent
commit
1a5d163dba
  1. 5
      .drone.yml
  2. 4
      src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs
  3. 11
      src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs
  4. 4
      src/Squidex.Domain.Apps.Entities/Apps/Services/IAppLimitsPlan.cs
  5. 4
      src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/ConfigAppLimitsPlan.cs
  6. 16
      src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/ConfigAppPlansProvider.cs
  7. 10
      src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs
  8. 10
      src/Squidex.Domain.Users/UserExtensions.cs
  9. 3
      src/Squidex.Domain.Users/UserManagerExtensions.cs
  10. 14
      src/Squidex.Infrastructure/EventSourcing/Grains/OrleansEventNotifier.cs
  11. 20
      src/Squidex.Infrastructure/Orleans/Bootstrap.cs
  12. 2
      src/Squidex.Shared/Identity/SquidexClaimTypes.cs
  13. 2
      src/Squidex.Shared/Users/IUserResolver.cs
  14. 11
      src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs
  15. 1
      src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs
  16. 2
      src/Squidex/Areas/Api/Controllers/Apps/Models/AssignAppContributorDto.cs
  17. 20
      src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorAssignedDto.cs
  18. 2
      src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs
  19. 3
      src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs
  20. 15
      src/Squidex/Areas/Api/Controllers/Plans/Models/PlanDto.cs
  21. 9
      src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs
  22. 1
      src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs
  23. 9
      src/Squidex/Areas/Api/Controllers/Users/Models/CreateUserDto.cs
  24. 26
      src/Squidex/Areas/Api/Controllers/Users/Models/PublicUserDto.cs
  25. 9
      src/Squidex/Areas/Api/Controllers/Users/Models/UpdateUserDto.cs
  26. 6
      src/Squidex/Areas/Api/Controllers/Users/Models/UserCreatedDto.cs
  27. 6
      src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs
  28. 4
      src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs
  29. 8
      src/Squidex/Areas/Api/Controllers/Users/UsersController.cs
  30. 2
      src/Squidex/Areas/IdentityServer/Controllers/Profile/ChangeProfileModel.cs
  31. 3
      src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs
  32. 2
      src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileVM.cs
  33. 12
      src/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml
  34. 3
      src/Squidex/Config/Domain/EventStoreServices.cs
  35. 10
      src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html
  36. 11
      src/Squidex/app/features/settings/pages/contributors/contributors-page.component.scss
  37. 15
      src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts
  38. 58
      src/Squidex/app/features/settings/pages/plans/plans-page.component.html
  39. 18
      src/Squidex/app/features/settings/pages/plans/plans-page.component.scss
  40. 7
      src/Squidex/app/framework/angular/forms/autocomplete.component.ts
  41. 40
      src/Squidex/app/shared/components/pipes.ts
  42. 6
      src/Squidex/app/shared/module.ts
  43. 13
      src/Squidex/app/shared/services/app-contributors.service.spec.ts
  44. 16
      src/Squidex/app/shared/services/app-contributors.service.ts
  45. 8
      src/Squidex/app/shared/services/plans.service.spec.ts
  46. 4
      src/Squidex/app/shared/services/plans.service.ts
  47. 20
      src/Squidex/app/shared/services/users-provider.service.spec.ts
  48. 10
      src/Squidex/app/shared/services/users-provider.service.ts
  49. 239
      src/Squidex/app/shared/services/users.service.spec.ts
  50. 129
      src/Squidex/app/shared/services/users.service.ts
  51. 10
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs
  52. 14
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/Billing/ConfigAppLimitsProviderTests.cs
  53. 34
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppContributorsTests.cs
  54. 2
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppTests.cs
  55. 1
      tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/OrleansEventNotifierTests.cs
  56. 39
      tests/Squidex.Infrastructure.Tests/Orleans/BootstrapTests.cs

5
.drone.yml

@ -1,3 +1,8 @@
clone:
git:
image: plugins/git:next
pull: true
pipeline: pipeline:
test_pull_request: test_pull_request:
image: docker image: docker

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:

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

@ -34,11 +34,17 @@ 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
{
command.ContributorId = user.Id;
if (contributors.TryGetValue(command.ContributorId, out var existing))
{ {
if (existing == command.Permission) if (existing == command.Permission)
{ {
@ -50,6 +56,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
error(new ValidationError("You have reached the maximum number of contributors for your plan.")); error(new ValidationError("You have reached the maximum number of contributors for your plan."));
} }
} }
}
}); });
} }

4
src/Squidex.Domain.Apps.Entities/Apps/Services/IAppLimitsPlan.cs

@ -15,6 +15,10 @@ namespace Squidex.Domain.Apps.Entities.Apps.Services
string Costs { get; } string Costs { get; }
string YearlyCosts { get; }
string YearlyId { get; }
long MaxApiCalls { get; } long MaxApiCalls { get; }
long MaxAssetSize { get; } long MaxAssetSize { get; }

4
src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/ConfigAppLimitsPlan.cs

@ -15,6 +15,10 @@ namespace Squidex.Domain.Apps.Entities.Apps.Services.Implementations
public string Costs { get; set; } public string Costs { get; set; }
public string YearlyCosts { get; set; }
public string YearlyId { get; set; }
public long MaxApiCalls { get; set; } public long MaxApiCalls { get; set; }
public long MaxAssetSize { get; set; } public long MaxAssetSize { get; set; }

16
src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/ConfigAppPlansProvider.cs

@ -23,15 +23,23 @@ namespace Squidex.Domain.Apps.Entities.Apps.Services.Implementations
MaxContributors = -1 MaxContributors = -1
}; };
private readonly Dictionary<string, ConfigAppLimitsPlan> plansById; private readonly Dictionary<string, ConfigAppLimitsPlan> plansById = new Dictionary<string, ConfigAppLimitsPlan>(StringComparer.OrdinalIgnoreCase);
private readonly List<ConfigAppLimitsPlan> plansList; private readonly List<ConfigAppLimitsPlan> plansList = new List<ConfigAppLimitsPlan>();
public ConfigAppPlansProvider(IEnumerable<ConfigAppLimitsPlan> config) public ConfigAppPlansProvider(IEnumerable<ConfigAppLimitsPlan> config)
{ {
Guard.NotNull(config, nameof(config)); Guard.NotNull(config, nameof(config));
plansList = config.Select(c => c.Clone()).OrderBy(x => x.MaxApiCalls).ToList(); foreach (var plan in config.OrderBy(x => x.MaxApiCalls).Select(x => x.Clone()))
plansById = plansList.ToDictionary(c => c.Id, StringComparer.OrdinalIgnoreCase); {
plansList.Add(plan);
plansById[plan.Id] = plan;
if (!string.IsNullOrWhiteSpace(plan.YearlyId) && !string.IsNullOrWhiteSpace(plan.YearlyCosts))
{
plansById[plan.YearlyId] = plan;
}
}
} }
public IEnumerable<IAppLimitsPlan> GetAvailablePlans() public IEnumerable<IAppLimitsPlan> GetAvailablePlans()

10
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,10 +73,17 @@ 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)
{
if (ObjectId.TryParse(id, out var parsed))
{ {
return await Collection.Find(x => x.Id == id).FirstOrDefaultAsync(); 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);

14
src/Squidex.Infrastructure/EventSourcing/Grains/OrleansEventNotifier.cs

@ -10,26 +10,24 @@ using Orleans;
namespace Squidex.Infrastructure.EventSourcing.Grains namespace Squidex.Infrastructure.EventSourcing.Grains
{ {
public sealed class OrleansEventNotifier : IEventNotifier, IInitializable public sealed class OrleansEventNotifier : IEventNotifier
{ {
private readonly IGrainFactory factory; private readonly IGrainFactory factory;
private IEventConsumerManagerGrain eventConsumerManagerGrain; private readonly Lazy<IEventConsumerManagerGrain> eventConsumerManagerGrain;
public OrleansEventNotifier(IGrainFactory factory) public OrleansEventNotifier(IGrainFactory factory)
{ {
Guard.NotNull(factory, nameof(factory)); Guard.NotNull(factory, nameof(factory));
this.factory = factory; eventConsumerManagerGrain = new Lazy<IEventConsumerManagerGrain>(() =>
}
public void Initialize()
{ {
eventConsumerManagerGrain = factory.GetGrain<IEventConsumerManagerGrain>("Default"); return factory.GetGrain<IEventConsumerManagerGrain>("Default");
});
} }
public void NotifyEventsStored(string streamName) public void NotifyEventsStored(string streamName)
{ {
eventConsumerManagerGrain?.ActivateAsync(streamName); eventConsumerManagerGrain.Value.ActivateAsync(streamName);
} }
public IDisposable Subscribe(Action<string> handler) public IDisposable Subscribe(Action<string> handler)

20
src/Squidex.Infrastructure/Orleans/Bootstrap.cs

@ -14,6 +14,7 @@ namespace Squidex.Infrastructure.Orleans
{ {
public sealed class Bootstrap<T> : IStartupTask where T : IBackgroundGrain public sealed class Bootstrap<T> : IStartupTask where T : IBackgroundGrain
{ {
private const int NumTries = 10;
private readonly IGrainFactory grainFactory; private readonly IGrainFactory grainFactory;
public Bootstrap(IGrainFactory grainFactory) public Bootstrap(IGrainFactory grainFactory)
@ -23,11 +24,26 @@ namespace Squidex.Infrastructure.Orleans
this.grainFactory = grainFactory; this.grainFactory = grainFactory;
} }
public Task Execute(CancellationToken cancellationToken) public async Task Execute(CancellationToken cancellationToken)
{
for (var i = 1; i <= NumTries; i++)
{
try
{ {
var grain = grainFactory.GetGrain<T>("Default"); var grain = grainFactory.GetGrain<T>("Default");
return grain.ActivateAsync(); await grain.ActivateAsync();
return;
}
catch (OrleansException)
{
if (i == NumTries)
{
throw;
}
}
}
} }
} }
} }

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>();

3
src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs

@ -55,11 +55,12 @@ namespace Squidex.Areas.Api.Controllers.Plans
public IActionResult GetPlans(string app) public IActionResult GetPlans(string app)
{ {
var planId = appPlansProvider.GetPlanForApp(App).Id; var planId = appPlansProvider.GetPlanForApp(App).Id;
var plans = appPlansProvider.GetAvailablePlans().Select(x => SimpleMapper.Map(x, new PlanDto())).ToList();
var response = new AppPlansDto var response = new AppPlansDto
{ {
CurrentPlanId = planId, CurrentPlanId = planId,
Plans = appPlansProvider.GetAvailablePlans().Select(x => SimpleMapper.Map(x, new PlanDto())).ToList(), Plans = plans,
PlanOwner = App.Plan?.Owner.Identifier, PlanOwner = App.Plan?.Owner.Identifier,
HasPortal = appPlansBillingManager.HasPortal HasPortal = appPlansBillingManager.HasPortal
}; };

15
src/Squidex/Areas/Api/Controllers/Plans/Models/PlanDto.cs

@ -5,6 +5,8 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.ComponentModel.DataAnnotations;
namespace Squidex.Areas.Api.Controllers.Plans.Models namespace Squidex.Areas.Api.Controllers.Plans.Models
{ {
public sealed class PlanDto public sealed class PlanDto
@ -12,18 +14,31 @@ namespace Squidex.Areas.Api.Controllers.Plans.Models
/// <summary> /// <summary>
/// The id of the plan. /// The id of the plan.
/// </summary> /// </summary>
[Required]
public string Id { get; set; } public string Id { get; set; }
/// <summary> /// <summary>
/// The name of the plan. /// The name of the plan.
/// </summary> /// </summary>
[Required]
public string Name { get; set; } public string Name { get; set; }
/// <summary> /// <summary>
/// The monthly costs of the plan. /// The monthly costs of the plan.
/// </summary> /// </summary>
[Required]
public string Costs { get; set; } public string Costs { get; set; }
/// <summary>
/// The yearly costs of the plan.
/// </summary>
public string YearlyCosts { get; set; }
/// <summary>
/// The yearly id of the plan.
/// </summary>
public string YearlyId { get; set; }
/// <summary> /// <summary>
/// The maximum number of API calls. /// The maximum number of API calls.
/// </summary> /// </summary>

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>

3
src/Squidex/Config/Domain/EventStoreServices.cs

@ -53,8 +53,7 @@ namespace Squidex.Config.Domain
}); });
services.AddSingletonAs<OrleansEventNotifier>() services.AddSingletonAs<OrleansEventNotifier>()
.As<IEventNotifier>() .As<IEventNotifier>();
.As<IInitializable>();
services.AddSingletonAs<DefaultStreamNameResolver>() services.AddSingletonAs<DefaultStreamNameResolver>()
.As<IStreamNameResolver>(); .As<IStreamNameResolver>();

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

@ -20,16 +20,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>
@ -43,12 +40,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">
<span class="autocomplete-user">
<img class="user-picture autocomplete-user-picture" [attr.src]="user | sqxUserDtoPicture" /> <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;
} }
} }

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

@ -15,8 +15,9 @@ import {
AppContributorsService, AppContributorsService,
AppsState, AppsState,
AutocompleteSource, AutocompleteSource,
UsersService, DialogService,
DialogService PublicUserDto,
UsersService
} from '@app/shared'; } from '@app/shared';
export class UsersDataSource implements AutocompleteSource { export class UsersDataSource implements AutocompleteSource {
@ -111,11 +112,17 @@ 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.appsState.appName, requestDto, this.appContributors.version) this.appContributorsService.postContributor(this.appsState.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.dialogs.notifyError(error); this.dialogs.notifyError(error);

58
src/Squidex/app/features/settings/pages/plans/plans-page.component.html

@ -1,20 +1,27 @@
<sqx-title message="{app} | Plans | Settings" parameter1="app" [value1]="appsState.appName"></sqx-title> <sqx-title message="{app} | Plans | Settings" parameter1="app" [value1]="ctx.appName"></sqx-title>
<sqx-panel desiredWidth="61rem"> <sqx-panel desiredWidth="61rem">
<ng-container title> <div class="panel-header">
Contributors <div class="panel-title-row">
</ng-container> <div class="float-right">
<ng-container menu>
<button class="btn btn-link btn-secondary" (click)="load(true)" title="Refresh Plans (CTRL + SHIFT + R)"> <button class="btn btn-link btn-secondary" (click)="load(true)" title="Refresh Plans (CTRL + SHIFT + R)">
<i class="icon-reset"></i> Refresh <i class="icon-reset"></i> Refresh
</button> </button>
<sqx-shortcut keys="ctrl+shift+r" (trigger)="load(true)"></sqx-shortcut> <sqx-shortcut keys="ctrl+shift+r" (trigger)="load(true)"></sqx-shortcut>
</ng-container> </div>
<h3 class="panel-title">Update Plan</h3>
</div>
<a class="panel-close" sqxParentLink>
<i class="icon-close"></i>
</a>
</div>
<ng-container content> <div class="panel-main">
<ng-container *ngIf="plans"> <div class="panel-content">
<div *ngIf="plans">
<div class="panel-alert panel-alert-danger" *ngIf="!planOwned"> <div class="panel-alert panel-alert-danger" *ngIf="!planOwned">
You have not created the subscription. Therefore you cannot change the plan. You have not created the subscription. Therefore you cannot change the plan.
</div> </div>
@ -25,24 +32,25 @@
<div class="clearfix"> <div class="clearfix">
<div class="card plan float-left" *ngFor="let plan of plans.plans"> <div class="card plan float-left" *ngFor="let plan of plans.plans">
<div class="card-body plan-header text-center"> <div class="card-header text-center">
<h4 class="card-title">{{plan.name}}</h4> <h4 class="card-title">{{plan.name}}</h4>
<h5 class="plan-price">{{plan.costs}}</h5> <h5 class="plan-price">{{plan.costs}}</h5>
<small class="text-muted">Per Month</small> <small class="text-muted">Per Month</small>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="plan-fact"> <div class="plan-fact text-center">
{{plan.maxApiCalls | sqxKNumber}} API Calls <div>
<strong>{{plan.maxApiCalls | sqxKNumber}}</strong> API Calls
</div> </div>
<div class="plan-fact"> <div>
{{plan.maxAssetSize | sqxFileSize}} Storage {{plan.maxAssetSize | sqxFileSize}} Storage
</div> </div>
<div class="plan-fact"> <div>
{{plan.maxContributors}} Contributors {{plan.maxContributors}} Contributors
</div> </div>
</div> </div>
<div class="card-body">
<button *ngIf="plan.id === plans.currentPlanId" class="btn btn-block btn-link btn-success plan-selected"> <button *ngIf="plan.id === plans.currentPlanId" class="btn btn-block btn-link btn-success plan-selected">
&#10003; Selected &#10003; Selected
</button> </button>
@ -51,12 +59,28 @@
Change Change
</button> </button>
</div> </div>
<div class="card-footer" *ngIf="plan.yearlyId">
<div class="text-center">
<h5 class="plan-price">{{plan.yearlyCosts}}</h5>
<small class="text-muted">Per Year</small>
</div>
<button *ngIf="plan.yearlyId === plans.currentPlanId" class="btn btn-block btn-link btn-success plan-selected">
&#10003; Selected
</button>
<button *ngIf="plan.yearlyId !== plans.currentPlanId" class="btn btn-block btn-success" [disabled]="isDisabled || !planOwned" (click)="changePlan(plan.yearlyId)">
Change
</button>
</div>
</div> </div>
</div> </div>
<div *ngIf="plans.hasPortal" class="billing-portal-link"> <div *ngIf="plans.hasPortal" class="billing-portal-link">
Go to <a target="_blank" href="{{portalUrl}}">Billing Portal</a> for payment history and subscription overview. Go to <a target="_blank" href="{{portalUrl}}">Billing Portal</a> for payment history and subscription overview.
</div> </div>
</ng-container> </div>
</ng-container> </div>
</div>
</sqx-panel> </sqx-panel>

18
src/Squidex/app/features/settings/pages/plans/plans-page.component.scss

@ -8,12 +8,10 @@
margin: .5rem; margin: .5rem;
} }
&-header {
border-bottom: 1px solid $color-border;
}
&-price { &-price {
color: $color-theme-blue; color: $color-theme-blue;
margin-top: 0;
margin-bottom: 0;
} }
&-selected { &-selected {
@ -21,8 +19,18 @@
} }
&-fact { &-fact {
line-height: 2rem; line-height: 1.8rem;
}
.btn {
margin-top: 1rem;
}
} }
.card-footer,
.card-header,
.card-body {
padding: 1rem;
} }
.empty { .empty {

7
src/Squidex/app/framework/angular/forms/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

@ -11,7 +11,7 @@ import { Observable, Subscription } from 'rxjs';
import { import {
ApiUrlConfig, ApiUrlConfig,
MathHelper, MathHelper,
UserDto, PublicUserDto,
UsersProviderService UsersProviderService
} from '@app/shared/internal'; } from '@app/shared/internal';
@ -91,42 +91,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
@ -137,7 +101,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,
@ -105,8 +101,6 @@ import {
MarkdownEditorComponent, MarkdownEditorComponent,
RouterModule, RouterModule,
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);
}) })

8
src/Squidex/app/shared/services/plans.service.spec.ts

@ -62,6 +62,8 @@ describe('PlansService', () => {
id: 'free', id: 'free',
name: 'Free', name: 'Free',
costs: '14 €', costs: '14 €',
yearlyId: 'free_yearly',
yearlyCosts: '12 €',
maxApiCalls: 1000, maxApiCalls: 1000,
maxAssetSize: 1500, maxAssetSize: 1500,
maxContributors: 2500 maxContributors: 2500
@ -70,6 +72,8 @@ describe('PlansService', () => {
id: 'prof', id: 'prof',
name: 'Prof', name: 'Prof',
costs: '18 €', costs: '18 €',
yearlyId: 'prof_yearly',
yearlyCosts: '16 €',
maxApiCalls: 4000, maxApiCalls: 4000,
maxAssetSize: 5500, maxAssetSize: 5500,
maxContributors: 6500 maxContributors: 6500
@ -87,8 +91,8 @@ describe('PlansService', () => {
'456', '456',
true, true,
[ [
new PlanDto('free', 'Free', '14 €', 1000, 1500, 2500), new PlanDto('free', 'Free', '14 €', 'free_yearly', '12 €', 1000, 1500, 2500),
new PlanDto('prof', 'Prof', '18 €', 4000, 5500, 6500) new PlanDto('prof', 'Prof', '18 €', 'prof_yearly', '16 €', 4000, 5500, 6500)
], ],
new Version('2') new Version('2')
)); ));

4
src/Squidex/app/shared/services/plans.service.ts

@ -44,6 +44,8 @@ export class PlanDto {
public readonly id: string, public readonly id: string,
public readonly name: string, public readonly name: string,
public readonly costs: string, public readonly costs: string,
public readonly yearlyId: string,
public readonly yearlyCosts: string,
public readonly maxApiCalls: number, public readonly maxApiCalls: number,
public readonly maxAssetSize: number, public readonly maxAssetSize: number,
public readonly maxContributors: number public readonly maxContributors: number
@ -92,6 +94,8 @@ export class PlansService {
item.id, item.id,
item.name, item.name,
item.costs, item.costs,
item.yearlyId,
item.yearlyCosts,
item.maxApiCalls, item.maxApiCalls,
item.maxAssetSize, item.maxAssetSize,
item.maxContributors); item.maxContributors);

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();

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

@ -10,10 +10,39 @@ import { inject, TestBed } from '@angular/core/testing';
import { import {
ApiUrlConfig, ApiUrlConfig,
CreateUserDto,
PublicUserDto,
UpdateUserDto,
UserDto, UserDto,
UserManagementService,
UsersDto,
UsersService UsersService
} from './../'; } from './../';
describe('UserDto', () => {
it('should update email and display name property when unlocking', () => {
const user_1 = new UserDto('1', 'sebastian@squidex.io', 'Sebastian', true);
const user_2 = user_1.update('qaisar@squidex.io', 'Qaisar');
expect(user_2.email).toEqual('qaisar@squidex.io');
expect(user_2.displayName).toEqual('Qaisar');
});
it('should update isLocked property when locking', () => {
const user_1 = new UserDto('1', 'sebastian@squidex.io', 'Sebastian', false);
const user_2 = user_1.lock();
expect(user_2.isLocked).toBeTruthy();
});
it('should update isLocked property when unlocking', () => {
const user_1 = new UserDto('1', 'sebastian@squidex.io', 'Sebastian', true);
const user_2 = user_1.unlock();
expect(user_2.isLocked).toBeFalsy();
});
});
describe('UsersService', () => { describe('UsersService', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
@ -34,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;
@ -48,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 PublicUserDto('123', 'User1'),
new UserDto('456', 'mail2@domain.com', 'User2', 'path/to/image2', true) new PublicUserDto('456', 'User2')
]); ]);
})); }));
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;
@ -84,6 +107,116 @@ describe('UsersService', () => {
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: '456',
displayName: 'User2'
}
]);
expect(users).toEqual(
[
new PublicUserDto('123', 'User1'),
new PublicUserDto('456', 'User2')
]);
}));
it('should make get request to get single user',
inject([UsersService, HttpTestingController], (usersService: UsersService, httpMock: HttpTestingController) => {
let user: PublicUserDto | null = null;
usersService.getUser('123').subscribe(result => {
user = result;
});
const req = httpMock.expectOne('http://service/p/api/users/123');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({ id: '123', displayName: 'User1' });
expect(user).toEqual(new PublicUserDto('123', 'User1'));
}));
});
describe('UserManagementService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
HttpClientTestingModule
],
providers: [
UserManagementService,
{ provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') }
]
});
});
afterEach(inject([HttpTestingController], (httpMock: HttpTestingController) => {
httpMock.verify();
}));
it('should make get request to get many users',
inject([UserManagementService, HttpTestingController], (userManagementService: UserManagementService, httpMock: HttpTestingController) => {
let users: UsersDto | null = null;
userManagementService.getUsers(20, 30).subscribe(result => {
users = result;
});
const req = httpMock.expectOne('http://service/p/api/user-management?take=20&skip=30&query=');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({
total: 100,
items: [
{
id: '123',
email: 'mail1@domain.com',
displayName: 'User1',
isLocked: true
},
{
id: '456',
email: 'mail2@domain.com',
displayName: 'User2',
isLocked: true
}
]
});
expect(users).toEqual(
new UsersDto(100, [
new UserDto('123', 'mail1@domain.com', 'User1', true),
new UserDto('456', 'mail2@domain.com', 'User2', true)
]));
}));
it('should make get request with query to get many users',
inject([UserManagementService, HttpTestingController], (userManagementService: UserManagementService, httpMock: HttpTestingController) => {
let users: UsersDto | null = null;
userManagementService.getUsers(20, 30, 'my-query').subscribe(result => {
users = result;
});
const req = httpMock.expectOne('http://service/p/api/user-management?take=20&skip=30&query=my-query');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({
total: 100,
items: [
{ {
id: '123', id: '123',
email: 'mail1@domain.com', email: 'mail1@domain.com',
@ -98,25 +231,26 @@ describe('UsersService', () => {
pictureUrl: 'path/to/image2', pictureUrl: 'path/to/image2',
isLocked: true isLocked: true
} }
]); ]
});
expect(users).toEqual( expect(users).toEqual(
[ 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)
]); ]));
})); }));
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([UserManagementService, HttpTestingController], (userManagementService: UserManagementService, httpMock: HttpTestingController) => {
let user: UserDto | null = null; let user: UserDto | null = null;
usersService.getUser('123').subscribe(result => { userManagementService.getUser('123').subscribe(result => {
user = result; user = result;
}); });
const req = httpMock.expectOne('http://service/p/api/users/123'); const req = httpMock.expectOne('http://service/p/api/user-management/123');
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();
@ -125,10 +259,71 @@ describe('UsersService', () => {
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',
inject([UserManagementService, HttpTestingController], (userManagementService: UserManagementService, httpMock: HttpTestingController) => {
const dto = new CreateUserDto('mail@squidex.io', 'Squidex User', 'password');
let user: UserDto | null = null;
userManagementService.postUser(dto).subscribe(result => {
user = result;
});
const req = httpMock.expectOne('http://service/p/api/user-management');
expect(req.request.method).toEqual('POST');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({ id: '123' });
expect(user).toEqual(new UserDto('123', dto.email, dto.displayName, false));
}));
it('should make put request to update user',
inject([UserManagementService, HttpTestingController], (userManagementService: UserManagementService, httpMock: HttpTestingController) => {
const dto = new UpdateUserDto('mail@squidex.io', 'Squidex User', 'password');
userManagementService.putUser('123', dto).subscribe();
const req = httpMock.expectOne('http://service/p/api/user-management/123');
expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({});
}));
it('should make put request to lock user',
inject([UserManagementService, HttpTestingController], (userManagementService: UserManagementService, httpMock: HttpTestingController) => {
userManagementService.lockUser('123').subscribe();
const req = httpMock.expectOne('http://service/p/api/user-management/123/lock');
expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({});
}));
it('should make put request to unlock user',
inject([UserManagementService, HttpTestingController], (userManagementService: UserManagementService, httpMock: HttpTestingController) => {
userManagementService.unlockUser('123').subscribe();
const req = httpMock.expectOne('http://service/p/api/user-management/123/unlock');
expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({});
})); }));
}); });

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

@ -13,15 +13,60 @@ import '@app/framework/angular/http/http-extensions';
import { ApiUrlConfig, HTTP } from '@app/framework'; import { ApiUrlConfig, HTTP } from '@app/framework';
export class UsersDto {
constructor(
public readonly total: number,
public readonly items: UserDto[]
) {
}
}
export class UserDto { export class UserDto {
constructor( constructor(
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 {
return new UserDto(this.id, email, displayName, this.isLocked);
}
public lock(): UserDto {
return new UserDto(this.id, this.email, this.displayName, true);
}
public unlock(): UserDto {
return new UserDto(this.id, this.email, this.displayName, false);
}
}
export class CreateUserDto {
constructor(
public readonly email: string,
public readonly displayName: string,
public readonly password: string
) {
}
}
export class UpdateUserDto {
constructor(
public readonly email: string,
public readonly displayName: string,
public readonly password: string
) {
}
}
export class PublicUserDto {
constructor(
public readonly id: string,
public readonly displayName: string
) {
}
} }
@Injectable() @Injectable()
@ -32,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)
@ -42,19 +87,61 @@ export class UsersService {
const items: any[] = body; const items: any[] = body;
return items.map(item => { return items.map(item => {
return new PublicUserDto(
item.id,
item.displayName);
});
})
.pretifyError('Failed to load users. Please reload.');
}
public getUser(id: string): Observable<PublicUserDto> {
const url = this.apiUrl.buildUrl(`api/users/${id}`);
return HTTP.getVersioned<any>(this.http, url)
.map(response => {
const body = response.payload.body;
return new PublicUserDto(
body.id,
body.displayName);
})
.pretifyError('Failed to load user. Please reload.');
}
}
@Injectable()
export class UserManagementService {
constructor(
private readonly http: HttpClient,
private readonly apiUrl: ApiUrlConfig
) {
}
public getUsers(take: number, skip: number, query?: string): Observable<UsersDto> {
const url = this.apiUrl.buildUrl(`api/user-management?take=${take}&skip=${skip}&query=${query || ''}`);
return HTTP.getVersioned<any>(this.http, url)
.map(response => {
const body = response.payload.body;
const items: any[] = body.items;
const users = items.map(item => {
return new UserDto( return new UserDto(
item.id, item.id,
item.email, item.email,
item.displayName, item.displayName,
item.pictureUrl,
item.isLocked); item.isLocked);
}); });
return new UsersDto(body.total, users);
}) })
.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<UserDto> {
const url = this.apiUrl.buildUrl(`api/users/${id}`); const url = this.apiUrl.buildUrl(`api/user-management/${id}`);
return HTTP.getVersioned<any>(this.http, url) return HTTP.getVersioned<any>(this.http, url)
.map(response => { .map(response => {
@ -64,9 +151,41 @@ export class UsersService {
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.');
} }
public postUser(dto: CreateUserDto): Observable<UserDto> {
const url = this.apiUrl.buildUrl('api/user-management');
return HTTP.postVersioned<any>(this.http, url, dto)
.map(response => {
const body = response.payload.body;
return new UserDto(body.id, dto.email, dto.displayName, false);
})
.pretifyError('Failed to create user. Please reload.');
}
public putUser(id: string, dto: UpdateUserDto): Observable<any> {
const url = this.apiUrl.buildUrl(`api/user-management/${id}`);
return HTTP.putVersioned(this.http, url, dto)
.pretifyError('Failed to update user. Please reload.');
}
public lockUser(id: string): Observable<any> {
const url = this.apiUrl.buildUrl(`api/user-management/${id}/lock`);
return HTTP.putVersioned(this.http, url, {})
.pretifyError('Failed to load users. Please retry.');
}
public unlockUser(id: string): Observable<any> {
const url = this.apiUrl.buildUrl(`api/user-management/${id}/unlock`);
return HTTP.putVersioned(this.http, url, {})
.pretifyError('Failed to load users. Please retry.');
}
} }

10
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";
@ -48,8 +49,11 @@ namespace Squidex.Domain.Apps.Entities.Apps
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]);

14
tests/Squidex.Domain.Apps.Entities.Tests/Apps/Billing/ConfigAppLimitsProviderTests.cs

@ -41,7 +41,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.Billing
Name = "Basic", Name = "Basic",
MaxApiCalls = 150000, MaxApiCalls = 150000,
MaxAssetSize = 1024 * 1024 * 2, MaxAssetSize = 1024 * 1024 * 2,
MaxContributors = 5 MaxContributors = 5,
YearlyCosts = "100€",
YearlyId = "basic_yearly"
}; };
private static readonly ConfigAppLimitsPlan[] Plans = { BasicPlan, FreePlan }; private static readonly ConfigAppLimitsPlan[] Plans = { BasicPlan, FreePlan };
@ -76,6 +78,16 @@ namespace Squidex.Domain.Apps.Entities.Apps.Billing
plan.ShouldBeEquivalentTo(BasicPlan); plan.ShouldBeEquivalentTo(BasicPlan);
} }
[Fact]
public void Should_return_fitting_yearly_app_plan()
{
var sut = new ConfigAppPlansProvider(Plans);
var plan = sut.GetPlanForApp(CreateApp("basic_yearly"));
plan.ShouldBeEquivalentTo(BasicPlan);
}
[Fact] [Fact]
public void Should_smallest_plan_if_none_fits() public void Should_smallest_plan_if_none_fits()
{ {

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"))

1
tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/OrleansEventNotifierTests.cs

@ -29,7 +29,6 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
[Fact] [Fact]
public void Should_wakeup_manager_with_stream_name() public void Should_wakeup_manager_with_stream_name()
{ {
sut.Initialize();
sut.NotifyEventsStored("my-stream"); sut.NotifyEventsStored("my-stream");
A.CallTo(() => manager.ActivateAsync("my-stream")) A.CallTo(() => manager.ActivateAsync("my-stream"))

39
tests/Squidex.Infrastructure.Tests/Orleans/BootstrapTests.cs

@ -6,10 +6,13 @@
// All rights reserved. // All rights reserved.
// ========================================================================== // ==========================================================================
using System;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using FakeItEasy; using FakeItEasy;
using Orleans; using Orleans;
using Orleans.Runtime;
using Squidex.Infrastructure.Tasks;
using Xunit; using Xunit;
namespace Squidex.Infrastructure.Orleans namespace Squidex.Infrastructure.Orleans
@ -37,5 +40,41 @@ namespace Squidex.Infrastructure.Orleans
A.CallTo(() => grain.ActivateAsync()) A.CallTo(() => grain.ActivateAsync())
.MustHaveHappened(); .MustHaveHappened();
} }
[Fact]
public async Task Should_fail_on_non_rejection_exception()
{
A.CallTo(() => grain.ActivateAsync())
.Throws(new InvalidOperationException());
await Assert.ThrowsAsync<InvalidOperationException>(() => sut.Execute(CancellationToken.None));
}
[Fact]
public async Task Should_retry_after_rejection_exception()
{
A.CallTo(() => grain.ActivateAsync())
.Returns(TaskHelper.Done);
A.CallTo(() => grain.ActivateAsync())
.Throws(new OrleansException()).Once();
await sut.Execute(CancellationToken.None);
A.CallTo(() => grain.ActivateAsync())
.MustHaveHappened(Repeated.Exactly.Twice);
}
[Fact]
public async Task Should_fail_after_10_rejection_exception()
{
A.CallTo(() => grain.ActivateAsync())
.Throws(new OrleansException());
await Assert.ThrowsAsync<OrleansException>(() => sut.Execute(CancellationToken.None));
A.CallTo(() => grain.ActivateAsync())
.MustHaveHappened(Repeated.Exactly.Times(10));
}
} }
} }

Loading…
Cancel
Save