From af645a420bda1931df6a5d78d31ce992851b43ad Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Thu, 29 Mar 2018 17:27:03 +0200 Subject: [PATCH 1/6] Improved privacy by hiding the email of the user. --- .../Apps/AppGrain.cs | 4 +- .../Apps/Guards/GuardAppContributors.cs | 23 ++++--- .../MongoUserStore.cs | 12 +++- src/Squidex.Domain.Users/UserExtensions.cs | 10 +++ .../UserManagerExtensions.cs | 3 +- .../Identity/SquidexClaimTypes.cs | 2 + src/Squidex.Shared/Users/IUserResolver.cs | 2 +- .../Apps/AppContributorsController.cs | 11 ++- .../Api/Controllers/Apps/AppsController.cs | 1 - .../Apps/Models/AssignAppContributorDto.cs | 2 +- .../Apps/Models/ContributorAssignedDto.cs | 20 ++++++ .../Controllers/Content/ContentsController.cs | 2 - .../Schemas/SchemaFieldsController.cs | 9 +-- .../Controllers/Schemas/SchemasController.cs | 1 - .../Controllers/Users/Models/CreateUserDto.cs | 9 +++ .../Controllers/Users/Models/PublicUserDto.cs | 26 +++++++ .../Controllers/Users/Models/UpdateUserDto.cs | 9 +++ .../Users/Models/UserCreatedDto.cs | 6 +- .../Api/Controllers/Users/Models/UserDto.cs | 6 -- .../Users/UserManagementController.cs | 4 +- .../Api/Controllers/Users/UsersController.cs | 8 +-- .../Controllers/Profile/ChangeProfileModel.cs | 2 + .../Controllers/Profile/ProfileController.cs | 3 +- .../Controllers/Profile/ProfileVM.cs | 2 + .../Views/Profile/Profile.cshtml | 12 +++- .../pages/users/user-page.component.ts | 1 - .../contributors-page.component.html | 14 ++-- .../contributors-page.component.scss | 11 ++- .../contributors-page.component.ts | 13 ++-- .../angular/autocomplete.component.ts | 7 +- src/Squidex/app/shared/components/pipes.ts | 40 +---------- src/Squidex/app/shared/module.ts | 6 -- .../services/app-contributors.service.spec.ts | 13 +++- .../services/app-contributors.service.ts | 16 ++++- .../services/users-provider.service.spec.ts | 20 +++--- .../shared/services/users-provider.service.ts | 10 +-- .../app/shared/services/users.service.spec.ts | 68 +++++++------------ .../app/shared/services/users.service.ts | 37 +++++----- .../Apps/AppGrainTests.cs | 12 ++-- .../Apps/Guards/GuardAppContributorsTests.cs | 34 ++++++++-- .../Apps/Guards/GuardAppTests.cs | 2 +- 41 files changed, 288 insertions(+), 205 deletions(-) create mode 100644 src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorAssignedDto.cs create mode 100644 src/Squidex/Areas/Api/Controllers/Users/Models/PublicUserDto.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs index 2f8d229e4..31b58c315 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs @@ -70,11 +70,13 @@ namespace Squidex.Domain.Apps.Entities.Apps }); 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)); AssignContributor(c); + + return EntityCreatedResult.Create(c.ContributorId, NewVersion); }); case RemoveContributor removeContributor: diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs index dfaedfc24..000b9dc10 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs @@ -34,20 +34,27 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards } 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))); } - 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.")); } } }); diff --git a/src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs b/src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs index 44cd87f61..89a6d6e55 100644 --- a/src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs +++ b/src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs @@ -12,6 +12,7 @@ using System.Security.Claims; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; +using MongoDB.Bson; using MongoDB.Driver; using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.Tasks; @@ -72,9 +73,16 @@ namespace Squidex.Domain.Users.MongoDb return new MongoUser { Email = email, UserName = email }; } - public async Task FindByIdAsync(string id) + public async Task 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 FindByIdAsync(string userId, CancellationToken cancellationToken) diff --git a/src/Squidex.Domain.Users/UserExtensions.cs b/src/Squidex.Domain.Users/UserExtensions.cs index cc87e88c7..1884648ce 100644 --- a/src/Squidex.Domain.Users/UserExtensions.cs +++ b/src/Squidex.Domain.Users/UserExtensions.cs @@ -35,6 +35,11 @@ namespace Squidex.Domain.Users 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) { user.SetClaim(SquidexClaimTypes.SquidexConsent, "true"); @@ -45,6 +50,11 @@ namespace Squidex.Domain.Users 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) { return user.HasClaimValue(SquidexClaimTypes.SquidexConsent, "true"); diff --git a/src/Squidex.Domain.Users/UserManagerExtensions.cs b/src/Squidex.Domain.Users/UserManagerExtensions.cs index 302b025d1..c84c61422 100644 --- a/src/Squidex.Domain.Users/UserManagerExtensions.cs +++ b/src/Squidex.Domain.Users/UserManagerExtensions.cs @@ -71,8 +71,9 @@ namespace Squidex.Domain.Users return user; } - public static Task UpdateAsync(this UserManager userManager, IUser user, string email, string displayName) + public static Task UpdateAsync(this UserManager userManager, IUser user, string email, string displayName, bool hidden) { + user.SetHidden(hidden); user.SetEmail(email); user.SetDisplayName(displayName); diff --git a/src/Squidex.Shared/Identity/SquidexClaimTypes.cs b/src/Squidex.Shared/Identity/SquidexClaimTypes.cs index b456cfd21..79d1bbf2d 100644 --- a/src/Squidex.Shared/Identity/SquidexClaimTypes.cs +++ b/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 SquidexHidden = "urn:squidex:hidden"; + public static readonly string Prefix = "urn:squidex:"; } } diff --git a/src/Squidex.Shared/Users/IUserResolver.cs b/src/Squidex.Shared/Users/IUserResolver.cs index 6dea877d9..5f2772e23 100644 --- a/src/Squidex.Shared/Users/IUserResolver.cs +++ b/src/Squidex.Shared/Users/IUserResolver.cs @@ -11,6 +11,6 @@ namespace Squidex.Shared.Users { public interface IUserResolver { - Task FindByIdAsync(string id); + Task FindByIdOrEmailAsync(string idOrEmail); } } diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs index 4f44f8288..7de8d6932 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs @@ -65,19 +65,24 @@ namespace Squidex.Areas.Api.Controllers.Apps /// The name of the app. /// Contributor object that needs to be added to the app. /// - /// 204 => User assigned to app. + /// 200 => User assigned to app. /// 400 => User is already assigned to the app or not found. /// 404 => App not found. /// [HttpPost] [Route("apps/{app}/contributors/")] + [ProducesResponseType(typeof(ContributorAssignedDto), 201)] [ProducesResponseType(typeof(ErrorDto), 400)] [ApiCosts(1)] public async Task 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>(); + var response = new ContributorAssignedDto { ContributorId = result.IdOrValue }; + + return Ok(response); } /// diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs index 66b178361..7304b11e3 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs @@ -99,7 +99,6 @@ namespace Squidex.Areas.Api.Controllers.Apps public async Task PostApp([FromBody] CreateAppDto request) { var command = SimpleMapper.Map(request, new CreateApp()); - var context = await CommandBus.PublishAsync(command); var result = context.Result>(); diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/AssignAppContributorDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/AssignAppContributorDto.cs index ed56a9183..3f0203d30 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/AssignAppContributorDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/Models/AssignAppContributorDto.cs @@ -15,7 +15,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models public sealed class AssignAppContributorDto { /// - /// The id of the user to add to the app. + /// The id or email of the user to add to the app. /// [Required] public string ContributorId { get; set; } diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorAssignedDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorAssignedDto.cs new file mode 100644 index 000000000..826fd9426 --- /dev/null +++ b/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 + { + /// + /// The id of the user that has been assigned as contributor. + /// + [Required] + public string ContributorId { get; set; } + } +} diff --git a/src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs b/src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs index f5a497941..e747e1b52 100644 --- a/src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs @@ -268,7 +268,6 @@ namespace Squidex.Areas.Api.Controllers.Contents await contentQuery.FindSchemaAsync(App, name); var command = new UpdateContent { ContentId = id, Data = request.ToCleaned() }; - var context = await CommandBus.PublishAsync(command); var result = context.Result(); @@ -301,7 +300,6 @@ namespace Squidex.Areas.Api.Controllers.Contents await contentQuery.FindSchemaAsync(App, name); var command = new PatchContent { ContentId = id, Data = request.ToCleaned() }; - var context = await CommandBus.PublishAsync(command); var result = context.Result(); diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs b/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs index 3a12a70d5..a2ebdb98d 100644 --- a/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs @@ -11,6 +11,7 @@ using NSwag.Annotations; using Squidex.Areas.Api.Controllers.Schemas.Models; using Squidex.Domain.Apps.Entities.Schemas.Commands; using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Reflection; using Squidex.Pipeline; namespace Squidex.Areas.Api.Controllers.Schemas @@ -50,13 +51,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas [ApiCosts(1)] public async Task PostField(string app, string name, [FromBody] AddFieldDto request) { - var command = new AddField - { - Name = request.Name, - Partitioning = request.Partitioning, - Properties = request.Properties.ToProperties() - }; - + var command = SimpleMapper.Map(request, new AddField { Properties = request.Properties.ToProperties() }); var context = await CommandBus.PublishAsync(command); var result = context.Result>(); diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs b/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs index 7118cc2a3..dcc5ffea1 100644 --- a/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs @@ -120,7 +120,6 @@ namespace Squidex.Areas.Api.Controllers.Schemas public async Task PostSchema(string app, [FromBody] CreateSchemaDto request) { var command = request.ToCommand(); - var context = await CommandBus.PublishAsync(command); var result = context.Result>(); diff --git a/src/Squidex/Areas/Api/Controllers/Users/Models/CreateUserDto.cs b/src/Squidex/Areas/Api/Controllers/Users/Models/CreateUserDto.cs index b67f308ef..3caaa052f 100644 --- a/src/Squidex/Areas/Api/Controllers/Users/Models/CreateUserDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Users/Models/CreateUserDto.cs @@ -11,13 +11,22 @@ namespace Squidex.Areas.Api.Controllers.Users.Models { public sealed class CreateUserDto { + /// + /// The email of the user. Unique value. + /// [Required] [EmailAddress] public string Email { get; set; } + /// + /// The display name (usually first name and last name) of the user. + /// [Required] public string DisplayName { get; set; } + /// + /// The password of the user. + /// [Required] public string Password { get; set; } } diff --git a/src/Squidex/Areas/Api/Controllers/Users/Models/PublicUserDto.cs b/src/Squidex/Areas/Api/Controllers/Users/Models/PublicUserDto.cs new file mode 100644 index 000000000..e398c88be --- /dev/null +++ b/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 + { + /// + /// The id of the user. + /// + [Required] + public string Id { get; set; } + + /// + /// The display name (usually first name and last name) of the user. + /// + [Required] + public string DisplayName { get; set; } + } +} diff --git a/src/Squidex/Areas/Api/Controllers/Users/Models/UpdateUserDto.cs b/src/Squidex/Areas/Api/Controllers/Users/Models/UpdateUserDto.cs index 049d62d47..e9511e2be 100644 --- a/src/Squidex/Areas/Api/Controllers/Users/Models/UpdateUserDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Users/Models/UpdateUserDto.cs @@ -11,13 +11,22 @@ namespace Squidex.Areas.Api.Controllers.Users.Models { public sealed class UpdateUserDto { + /// + /// The email of the user. Unique value. + /// [Required] [EmailAddress] public string Email { get; set; } + /// + /// The display name (usually first name and last name) of the user. + /// [Required] public string DisplayName { get; set; } + /// + /// The password of the user. + /// public string Password { get; set; } } } diff --git a/src/Squidex/Areas/Api/Controllers/Users/Models/UserCreatedDto.cs b/src/Squidex/Areas/Api/Controllers/Users/Models/UserCreatedDto.cs index a4134357f..815c7b533 100644 --- a/src/Squidex/Areas/Api/Controllers/Users/Models/UserCreatedDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Users/Models/UserCreatedDto.cs @@ -11,10 +11,10 @@ namespace Squidex.Areas.Api.Controllers.Users.Models { public sealed class UserCreatedDto { + /// + /// The id of the user. + /// [Required] public string Id { get; set; } - - [Required] - public string PictureUrl { get; set; } } } diff --git a/src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs b/src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs index c210416ef..42407d14b 100644 --- a/src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs @@ -23,12 +23,6 @@ namespace Squidex.Areas.Api.Controllers.Users.Models [Required] public string Email { get; set; } - /// - /// The url to the profile picture of the user. - /// - [Required] - public string PictureUrl { get; set; } - /// /// The display name (usually first name and last name) of the user. /// diff --git a/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs b/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs index d77397666..b3d7c33a7 100644 --- a/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs +++ b/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 response = new UserCreatedDto { Id = user.Id, PictureUrl = user.PictureUrl() }; + var response = new UserCreatedDto { Id = user.Id }; return Ok(response); } @@ -129,7 +129,7 @@ namespace Squidex.Areas.Api.Controllers.Users 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) diff --git a/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs b/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs index a69b126f3..9325f0c1c 100644 --- a/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs +++ b/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs @@ -65,12 +65,12 @@ namespace Squidex.Areas.Api.Controllers.Users [ApiAuthorize] [HttpGet] [Route("users/")] - [ProducesResponseType(typeof(UserDto[]), 200)] + [ProducesResponseType(typeof(PublicUserDto[]), 200)] public async Task GetUsers(string query) { 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); } @@ -86,7 +86,7 @@ namespace Squidex.Areas.Api.Controllers.Users [ApiAuthorize] [HttpGet] [Route("users/{id}/")] - [ProducesResponseType(typeof(UserDto), 200)] + [ProducesResponseType(typeof(PublicUserDto), 200)] public async Task GetUser(string id) { var entity = await userManager.FindByIdAsync(id); @@ -96,7 +96,7 @@ namespace Squidex.Areas.Api.Controllers.Users 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); } diff --git a/src/Squidex/Areas/IdentityServer/Controllers/Profile/ChangeProfileModel.cs b/src/Squidex/Areas/IdentityServer/Controllers/Profile/ChangeProfileModel.cs index cb473d902..e1eb3c23e 100644 --- a/src/Squidex/Areas/IdentityServer/Controllers/Profile/ChangeProfileModel.cs +++ b/src/Squidex/Areas/IdentityServer/Controllers/Profile/ChangeProfileModel.cs @@ -17,5 +17,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile [Required(ErrorMessage = "DisplayName is required.")] public string DisplayName { get; set; } + + public bool IsHidden { get; set; } } } diff --git a/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs b/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs index 90ea54e4f..7d242ccbb 100644 --- a/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs +++ b/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs @@ -83,7 +83,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile [Route("/account/profile/update/")] public Task 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."); } @@ -195,6 +195,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile ExternalLogins = user.Logins, ExternalProviders = externalProviders, DisplayName = user.DisplayName(), + IsHidden = user.IsHidden(), HasPassword = await userManager.HasPasswordAsync(user), HasPasswordAuth = identityOptions.Value.AllowPasswordAuth, SuccessMessage = successMessage diff --git a/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileVM.cs b/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileVM.cs index 052cc9bc9..0ac09080c 100644 --- a/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileVM.cs +++ b/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileVM.cs @@ -22,6 +22,8 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile public string SuccessMessage { get; set; } + public bool IsHidden { get; set; } + public bool HasPassword { get; set; } public bool HasPasswordAuth { get; set; } diff --git a/src/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml b/src/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml index 7150c41c7..80574e1e3 100644 --- a/src/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml +++ b/src/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml @@ -52,7 +52,7 @@ } - +
@@ -65,7 +65,15 @@
} - + + + +
+
+ + + +
diff --git a/src/Squidex/app/features/administration/pages/users/user-page.component.ts b/src/Squidex/app/features/administration/pages/users/user-page.component.ts index deddd7689..d506ba387 100644 --- a/src/Squidex/app/features/administration/pages/users/user-page.component.ts +++ b/src/Squidex/app/features/administration/pages/users/user-page.component.ts @@ -68,7 +68,6 @@ export class UserPageComponent implements OnInit { created.id, requestDto.email, requestDto.displayName, - created.pictureUrl!, false); this.ctx.notifyInfo('User created successfully.'); diff --git a/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html b/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html index 95c0e541b..83f590a0b 100644 --- a/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html +++ b/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html @@ -27,16 +27,13 @@ {{contributor.contributorId | sqxUserName}} - - {{contributor.contributorId | sqxUserEmail}} - - @@ -50,12 +47,13 @@
- + - + + - {{user.displayName}} - {{user.email}} + {{user.displayName}} +
diff --git a/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.scss b/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.scss index 7cc270ddf..e42bfaa5c 100644 --- a/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.scss +++ b/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.scss @@ -2,14 +2,11 @@ @import '_mixins'; .autocomplete-user { - &-picture { - float: left; - margin-top: .4rem; + & { + @include truncate; } - &-name, - &-email { - @include truncate; - margin-left: 3rem; + &-name { + margin-left: .25rem; } } \ No newline at end of file diff --git a/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts b/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts index 5880f4053..b644f5cad 100644 --- a/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts +++ b/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts @@ -16,6 +16,7 @@ import { AppContributorsService, AutocompleteSource, HistoryChannelUpdated, + PublicUserDto, UsersService } from 'shared'; @@ -110,16 +111,20 @@ export class ContributorsPageComponent implements OnInit { } 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) .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(); }, error => { this.ctx.notifyError(error); - - this.resetContributorForm(); }); } diff --git a/src/Squidex/app/framework/angular/autocomplete.component.ts b/src/Squidex/app/framework/angular/autocomplete.component.ts index 4400e2959..8b4b74aca 100644 --- a/src/Squidex/app/framework/angular/autocomplete.component.ts +++ b/src/Squidex/app/framework/angular/autocomplete.component.ts @@ -61,15 +61,18 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O public ngOnInit() { this.subscription = this.queryInput.valueChanges + .do(query => { + this.callChange(query); + }) .map(query => query) .map(query => query ? query.trim() : query) - .distinctUntilChanged() - .debounceTime(200) .do(query => { if (!query) { this.reset(); } }) + .distinctUntilChanged() + .debounceTime(200) .filter(query => !!query && !!this.source) .switchMap(query => this.source.find(query)).catch(error => Observable.of([])) .subscribe(items => { diff --git a/src/Squidex/app/shared/components/pipes.ts b/src/Squidex/app/shared/components/pipes.ts index f2b641f3c..54198593b 100644 --- a/src/Squidex/app/shared/components/pipes.ts +++ b/src/Squidex/app/shared/components/pipes.ts @@ -10,7 +10,7 @@ import { Observable, Subscription } from 'rxjs'; import { ApiUrlConfig, MathHelper } from 'framework'; -import { UserDto, UsersProviderService } from './../declarations-base'; +import { PublicUserDto, UsersProviderService } from './../declarations-base'; class UserAsyncPipe implements OnDestroy { 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({ name: 'sqxUserDtoPicture', 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`); } } diff --git a/src/Squidex/app/shared/module.ts b/src/Squidex/app/shared/module.ts index aa11176e0..8085f13f6 100644 --- a/src/Squidex/app/shared/module.ts +++ b/src/Squidex/app/shared/module.ts @@ -53,8 +53,6 @@ import { UnsetAppGuard, UsagesService, UserDtoPicture, - UserEmailPipe, - UserEmailRefPipe, UserNamePipe, UserNameRefPipe, UserIdPicturePipe, @@ -83,8 +81,6 @@ import { LanguageSelectorComponent, MarkdownEditorComponent, UserDtoPicture, - UserEmailPipe, - UserEmailRefPipe, UserIdPicturePipe, UserNamePipe, UserNameRefPipe, @@ -104,8 +100,6 @@ import { LanguageSelectorComponent, MarkdownEditorComponent, UserDtoPicture, - UserEmailPipe, - UserEmailRefPipe, UserIdPicturePipe, UserNamePipe, UserNameRefPipe, diff --git a/src/Squidex/app/shared/services/app-contributors.service.spec.ts b/src/Squidex/app/shared/services/app-contributors.service.spec.ts index d27583e4e..548e009eb 100644 --- a/src/Squidex/app/shared/services/app-contributors.service.spec.ts +++ b/src/Squidex/app/shared/services/app-contributors.service.spec.ts @@ -14,7 +14,8 @@ import { AppContributorDto, AppContributorsDto, AppContributorsService, - Version + Version, + ContributorAssignedDto } from './../'; describe('AppContributorsDto', () => { @@ -122,14 +123,20 @@ describe('AppContributorsService', () => { 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'); expect(req.request.method).toEqual('POST'); 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', diff --git a/src/Squidex/app/shared/services/app-contributors.service.ts b/src/Squidex/app/shared/services/app-contributors.service.ts index c3fd8e112..cd66bd2ec 100644 --- a/src/Squidex/app/shared/services/app-contributors.service.ts +++ b/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() export class AppContributorsService { constructor( @@ -87,10 +94,17 @@ export class AppContributorsService { .pretifyError('Failed to load contributors. Please reload.'); } - public postContributor(appName: string, dto: AppContributorDto, version: Version): Observable> { + public postContributor(appName: string, dto: AppContributorDto, version: Version): Observable> { const url = this.apiUrl.buildUrl(`api/apps/${appName}/contributors`); 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(() => { this.analytics.trackEvent('Contributor', 'Configured', appName); }) diff --git a/src/Squidex/app/shared/services/users-provider.service.spec.ts b/src/Squidex/app/shared/services/users-provider.service.spec.ts index 7f5a08060..41a903f8b 100644 --- a/src/Squidex/app/shared/services/users-provider.service.spec.ts +++ b/src/Squidex/app/shared/services/users-provider.service.spec.ts @@ -11,7 +11,7 @@ import { IMock, Mock, Times } from 'typemoq'; import { AuthService, Profile, - UserDto, + PublicUserDto, UsersProviderService, UsersService } from './../'; @@ -28,12 +28,12 @@ describe('UsersProviderService', () => { }); 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')) .returns(() => Observable.of(user)).verifiable(Times.once()); - let resultingUser: UserDto | null = null; + let resultingUser: PublicUserDto | null = null; usersProviderService.getUser('123').subscribe(result => { resultingUser = result; @@ -45,14 +45,14 @@ describe('UsersProviderService', () => { }); 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')) .returns(() => Observable.of(user)).verifiable(Times.once()); usersProviderService.getUser('123'); - let resultingUser: UserDto | null = null; + let resultingUser: PublicUserDto | null = null; usersProviderService.getUser('123').subscribe(result => { resultingUser = result; @@ -64,7 +64,7 @@ describe('UsersProviderService', () => { }); 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) .returns(() => new Profile({ profile: { sub: '123'}})); @@ -72,13 +72,13 @@ describe('UsersProviderService', () => { usersService.setup(x => x.getUser('123')) .returns(() => Observable.of(user)).verifiable(Times.once()); - let resultingUser: UserDto | null = null; + let resultingUser: PublicUserDto | null = null; usersProviderService.getUser('123').subscribe(result => { resultingUser = result; }).unsubscribe(); - expect(resultingUser).toEqual(new UserDto('123', 'mail@domain.com', 'Me', 'path/to/image', true)); + expect(resultingUser).toEqual(new PublicUserDto('123', 'Me')); usersService.verifyAll(); }); @@ -90,13 +90,13 @@ describe('UsersProviderService', () => { usersService.setup(x => x.getUser('123')) .returns(() => Observable.throw('NOT FOUND')).verifiable(Times.once()); - let resultingUser: UserDto | null = null; + let resultingUser: PublicUserDto | null = null; usersProviderService.getUser('123').subscribe(result => { resultingUser = result; }).unsubscribe(); - expect(resultingUser).toEqual(new UserDto('NOT FOUND', 'NOT FOUND', 'NOT FOUND', null, false)); + expect(resultingUser).toEqual(new PublicUserDto('unknown', 'unknown')); usersService.verifyAll(); }); diff --git a/src/Squidex/app/shared/services/users-provider.service.ts b/src/Squidex/app/shared/services/users-provider.service.ts index a4be6cb6d..87f9f1db9 100644 --- a/src/Squidex/app/shared/services/users-provider.service.ts +++ b/src/Squidex/app/shared/services/users-provider.service.ts @@ -8,13 +8,13 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; -import { UserDto, UsersService } from './users.service'; +import { PublicUserDto, UsersService } from './users.service'; import { AuthService } from './auth.service'; @Injectable() export class UsersProviderService { - private readonly caches: { [id: string]: Observable } = {}; + private readonly caches: { [id: string]: Observable } = {}; constructor( private readonly usersService: UsersService, @@ -22,14 +22,14 @@ export class UsersProviderService { ) { } - public getUser(id: string, me: string | null = 'Me'): Observable { + public getUser(id: string, me: string | null = 'Me'): Observable { let result = this.caches[id]; if (!result) { const request = this.usersService.getUser(id) .catch(error => { - return Observable.of(new UserDto('NOT FOUND', 'NOT FOUND', 'NOT FOUND', null, false)); + return Observable.of(new PublicUserDto('Unknown', 'Unknown')); }) .publishLast(); @@ -41,7 +41,7 @@ export class UsersProviderService { return result .map(dto => { 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; }).share(); diff --git a/src/Squidex/app/shared/services/users.service.spec.ts b/src/Squidex/app/shared/services/users.service.spec.ts index 2173d573a..de4a133ce 100644 --- a/src/Squidex/app/shared/services/users.service.spec.ts +++ b/src/Squidex/app/shared/services/users.service.spec.ts @@ -11,6 +11,7 @@ import { inject, TestBed } from '@angular/core/testing'; import { ApiUrlConfig, CreateUserDto, + PublicUserDto, UpdateUserDto, UserDto, UserManagementService, @@ -20,7 +21,7 @@ import { describe('UserDto', () => { 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'); expect(user_2.email).toEqual('qaisar@squidex.io'); @@ -28,14 +29,14 @@ describe('UserDto', () => { }); 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(); expect(user_2.isLocked).toBeTruthy(); }); 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(); expect(user_2.isLocked).toBeFalsy(); @@ -62,7 +63,7 @@ describe('UsersService', () => { it('should make get request to get many users', inject([UsersService, HttpTestingController], (usersService: UsersService, httpMock: HttpTestingController) => { - let users: UserDto[] | null = null; + let users: PublicUserDto[] | null = null; usersService.getUsers().subscribe(result => { users = result; @@ -76,31 +77,25 @@ describe('UsersService', () => { req.flush([ { id: '123', - email: 'mail1@domain.com', - displayName: 'User1', - pictureUrl: 'path/to/image1', - isLocked: true + displayName: 'User1' }, { id: '456', - email: 'mail2@domain.com', - displayName: 'User2', - pictureUrl: 'path/to/image2', - isLocked: true + displayName: 'User2' } ]); expect(users).toEqual( [ - new UserDto('123', 'mail1@domain.com', 'User1', 'path/to/image1', true), - new UserDto('456', 'mail2@domain.com', 'User2', 'path/to/image2', true) + 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([UsersService, HttpTestingController], (usersService: UsersService, httpMock: HttpTestingController) => { - let users: UserDto[] | null = null; + let users: PublicUserDto[] | null = null; usersService.getUsers('my-query').subscribe(result => { users = result; @@ -114,31 +109,25 @@ describe('UsersService', () => { req.flush([ { id: '123', - email: 'mail1@domain.com', - displayName: 'User1', - pictureUrl: 'path/to/image1', - isLocked: true + displayName: 'User1' }, { id: '456', - email: 'mail2@domain.com', - displayName: 'User2', - pictureUrl: 'path/to/image2', - isLocked: true + displayName: 'User2' } ]); expect(users).toEqual( [ - new UserDto('123', 'mail1@domain.com', 'User1', 'path/to/image1', true), - new UserDto('456', 'mail2@domain.com', 'User2', 'path/to/image2', true) + 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: UserDto | null = null; + let user: PublicUserDto | null = null; usersService.getUser('123').subscribe(result => { user = result; @@ -149,15 +138,9 @@ describe('UsersService', () => { expect(req.request.method).toEqual('GET'); expect(req.request.headers.get('If-Match')).toBeNull(); - req.flush({ - id: '123', - email: 'mail1@domain.com', - displayName: 'User1', - pictureUrl: 'path/to/image1', - isLocked: true - }); + req.flush({ id: '123', displayName: 'User1' }); - 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', email: 'mail1@domain.com', displayName: 'User1', - pictureUrl: 'path/to/image1', isLocked: true }, { id: '456', email: 'mail2@domain.com', displayName: 'User2', - pictureUrl: 'path/to/image2', isLocked: true } ] @@ -214,8 +195,8 @@ describe('UserManagementService', () => { expect(users).toEqual( new UsersDto(100, [ - new UserDto('123', 'mail1@domain.com', 'User1', 'path/to/image1', true), - new UserDto('456', 'mail2@domain.com', 'User2', 'path/to/image2', true) + new UserDto('123', 'mail1@domain.com', 'User1', true), + new UserDto('456', 'mail2@domain.com', 'User2', true) ])); })); @@ -255,8 +236,8 @@ describe('UserManagementService', () => { expect(users).toEqual( new UsersDto(100, [ - new UserDto('123', 'mail1@domain.com', 'User1', 'path/to/image1', true), - new UserDto('456', 'mail2@domain.com', 'User2', 'path/to/image2', true) + new UserDto('123', 'mail1@domain.com', 'User1', true), + new UserDto('456', 'mail2@domain.com', 'User2', true) ])); })); @@ -278,11 +259,10 @@ describe('UserManagementService', () => { 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 UserDto('123', 'mail1@domain.com', 'User1', true)); })); it('should make post request to create user', @@ -301,9 +281,9 @@ describe('UserManagementService', () => { expect(req.request.method).toEqual('POST'); 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', diff --git a/src/Squidex/app/shared/services/users.service.ts b/src/Squidex/app/shared/services/users.service.ts index bc11c29ec..2e360d04b 100644 --- a/src/Squidex/app/shared/services/users.service.ts +++ b/src/Squidex/app/shared/services/users.service.ts @@ -26,21 +26,20 @@ export class UserDto { public readonly id: string, public readonly email: string, public readonly displayName: string, - public readonly pictureUrl: string | null, public readonly isLocked: boolean ) { } 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 { - 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 { - 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() export class UsersService { constructor( @@ -70,7 +77,7 @@ export class UsersService { ) { } - public getUsers(query?: string): Observable { + public getUsers(query?: string): Observable { const url = this.apiUrl.buildUrl(`api/users?query=${query || ''}`); return HTTP.getVersioned(this.http, url) @@ -80,30 +87,24 @@ export class UsersService { const items: any[] = body; return items.map(item => { - return new UserDto( + return new PublicUserDto( item.id, - item.email, - item.displayName, - item.pictureUrl, - item.isLocked); + item.displayName); }); }) .pretifyError('Failed to load users. Please reload.'); } - public getUser(id: string): Observable { + public getUser(id: string): Observable { const url = this.apiUrl.buildUrl(`api/users/${id}`); return HTTP.getVersioned(this.http, url) .map(response => { const body = response.payload.body; - return new UserDto( + return new PublicUserDto( body.id, - body.email, - body.displayName, - body.pictureUrl, - body.isLocked); + body.displayName); }) .pretifyError('Failed to load user. Please reload.'); } @@ -131,7 +132,6 @@ export class UserManagementService { item.id, item.email, item.displayName, - item.pictureUrl, item.isLocked); }); @@ -151,7 +151,6 @@ export class UserManagementService { body.id, body.email, body.displayName, - body.pictureUrl, body.isLocked); }) .pretifyError('Failed to load user. Please reload.'); @@ -164,7 +163,7 @@ export class UserManagementService { .map(response => { 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.'); } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs index 326b854a4..539d7778d 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs +++ b/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(); private readonly IAppPlansProvider appPlansProvider = A.Fake(); private readonly IAppPlanBillingManager appPlansBillingManager = A.Fake(); + private readonly IUser user = A.Fake(); private readonly IUserResolver userResolver = A.Fake(); private readonly string contributorId = Guid.NewGuid().ToString(); private readonly string clientId = "client"; @@ -46,10 +47,13 @@ namespace Squidex.Domain.Apps.Entities.Apps public AppGrainTests() { A.CallTo(() => appProvider.GetAppAsync(AppName)) - .Returns((IAppEntity)null); + .Returns((IAppEntity)null); - A.CallTo(() => userResolver.FindByIdAsync(contributorId)) - .Returns(A.Fake()); + A.CallTo(() => user.Id) + .Returns(contributorId); + + A.CallTo(() => userResolver.FindByIdOrEmailAsync(contributorId)) + .Returns(user); initialPatterns = new InitialPatterns { @@ -163,7 +167,7 @@ namespace Squidex.Domain.Apps.Entities.Apps 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]); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppContributorsTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppContributorsTests.cs index c674c5cb6..e5fc486de 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppContributorsTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppContributorsTests.cs @@ -20,14 +20,29 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards { public class GuardAppContributorsTests { + private readonly IUser user1 = A.Fake(); + private readonly IUser user2 = A.Fake(); + private readonly IUser user3 = A.Fake(); private readonly IUserResolver users = A.Fake(); private readonly IAppLimitsPlan appPlan = A.Fake(); private readonly AppContributors contributors_0 = AppContributors.Empty; public GuardAppContributorsTests() { - A.CallTo(() => users.FindByIdAsync(A.Ignored)) - .Returns(A.Fake()); + A.CallTo(() => user1.Id).Returns("1"); + 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(null)); A.CallTo(() => appPlan.MaxContributors) .Returns(10); @@ -62,10 +77,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards [Fact] public Task CanAssign_should_throw_exception_if_user_not_found() { - A.CallTo(() => users.FindByIdAsync(A.Ignored)) - .Returns(Task.FromResult(null)); - - var command = new AssignContributor { ContributorId = "1", Permission = (AppContributorPermission)10 }; + var command = new AssignContributor { ContributorId = "notfound", Permission = (AppContributorPermission)10 }; return Assert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_0, command, users, appPlan)); } @@ -84,6 +96,16 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards return Assert.ThrowsAsync(() => 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] public Task CanAssign_should_not_throw_exception_if_user_found() { diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppTests.cs index ea8bc14c7..237000952 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppTests.cs +++ b/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")) .Returns(Task.FromResult(null)); - A.CallTo(() => users.FindByIdAsync(A.Ignored)) + A.CallTo(() => users.FindByIdOrEmailAsync(A.Ignored)) .Returns(A.Fake()); A.CallTo(() => appPlans.GetPlan("free")) From fab2a7cc194ba0fcc695be9e81fa8f3e82b94de3 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Thu, 29 Mar 2018 17:58:53 +0200 Subject: [PATCH 2/6] Tests fixed --- .../app/shared/services/users-provider.service.spec.ts | 2 +- src/Squidex/app/shared/services/users.service.spec.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Squidex/app/shared/services/users-provider.service.spec.ts b/src/Squidex/app/shared/services/users-provider.service.spec.ts index 41a903f8b..78d46dace 100644 --- a/src/Squidex/app/shared/services/users-provider.service.spec.ts +++ b/src/Squidex/app/shared/services/users-provider.service.spec.ts @@ -96,7 +96,7 @@ describe('UsersProviderService', () => { resultingUser = result; }).unsubscribe(); - expect(resultingUser).toEqual(new PublicUserDto('unknown', 'unknown')); + expect(resultingUser).toEqual(new PublicUserDto('Unknown', 'Unknown')); usersService.verifyAll(); }); diff --git a/src/Squidex/app/shared/services/users.service.spec.ts b/src/Squidex/app/shared/services/users.service.spec.ts index de4a133ce..af54d971b 100644 --- a/src/Squidex/app/shared/services/users.service.spec.ts +++ b/src/Squidex/app/shared/services/users.service.spec.ts @@ -87,8 +87,8 @@ describe('UsersService', () => { expect(users).toEqual( [ - new UserDto('123', 'mail1@domain.com', 'User1', true), - new UserDto('456', 'mail2@domain.com', 'User2', true) + new PublicUserDto('123', 'User1'), + new PublicUserDto('456', 'User2') ]); })); From b0f3666786bd0cccef07b1c27ed714bc0c475d0e Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Thu, 29 Mar 2018 18:25:02 +0200 Subject: [PATCH 3/6] Fore build --- LICENSE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE.txt b/LICENSE.txt index 95317685c..306b9eced 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. \ No newline at end of file From ee4ceabf8a889ad9e89f5f6efcef4e857289149a Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Thu, 29 Mar 2018 18:45:17 +0200 Subject: [PATCH 4/6] Git test --- .drone.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.drone.yml b/.drone.yml index 389a52a4a..93bc57bd1 100644 --- a/.drone.yml +++ b/.drone.yml @@ -1,3 +1,8 @@ +clone: + git: + image: plugins/git:next + pull: true + pipeline: test_pull_request: image: docker From 2a13dd4c516776d584bb6cd1218f006bbb143338 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sun, 8 Apr 2018 15:45:56 +0200 Subject: [PATCH 5/6] Yearly plans. --- .../Apps/Services/IAppLimitsPlan.cs | 4 ++ .../Implementations/ConfigAppLimitsPlan.cs | 4 ++ .../Implementations/ConfigAppPlansProvider.cs | 16 ++++++-- .../Grains/OrleansEventNotifier.cs | 16 ++++---- .../Orleans/Bootstrap.cs | 22 ++++++++-- .../Controllers/Plans/AppPlansController.cs | 3 +- .../Api/Controllers/Plans/Models/PlanDto.cs | 15 +++++++ .../Config/Domain/EventStoreServices.cs | 3 +- .../pages/plans/plans-page.component.html | 40 +++++++++++++------ .../pages/plans/plans-page.component.scss | 18 ++++++--- .../app/shared/services/plans.service.spec.ts | 8 +++- .../app/shared/services/plans.service.ts | 4 ++ .../Billing/ConfigAppLimitsProviderTests.cs | 14 ++++++- 13 files changed, 128 insertions(+), 39 deletions(-) diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppLimitsPlan.cs b/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppLimitsPlan.cs index 21bbae9bf..59d0feed4 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppLimitsPlan.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Services/IAppLimitsPlan.cs @@ -15,6 +15,10 @@ namespace Squidex.Domain.Apps.Entities.Apps.Services string Costs { get; } + string YearlyCosts { get; } + + string YearlyId { get; } + long MaxApiCalls { get; } long MaxAssetSize { get; } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/ConfigAppLimitsPlan.cs b/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/ConfigAppLimitsPlan.cs index 5f4892e4b..3d568c928 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/ConfigAppLimitsPlan.cs +++ b/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 YearlyCosts { get; set; } + + public string YearlyId { get; set; } + public long MaxApiCalls { get; set; } public long MaxAssetSize { get; set; } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/ConfigAppPlansProvider.cs b/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/ConfigAppPlansProvider.cs index 813914f09..83bd9d196 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/ConfigAppPlansProvider.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/ConfigAppPlansProvider.cs @@ -23,15 +23,23 @@ namespace Squidex.Domain.Apps.Entities.Apps.Services.Implementations MaxContributors = -1 }; - private readonly Dictionary plansById; - private readonly List plansList; + private readonly Dictionary plansById = new Dictionary(StringComparer.OrdinalIgnoreCase); + private readonly List plansList = new List(); public ConfigAppPlansProvider(IEnumerable config) { Guard.NotNull(config, nameof(config)); - plansList = config.Select(c => c.Clone()).OrderBy(x => x.MaxApiCalls).ToList(); - plansById = plansList.ToDictionary(c => c.Id, StringComparer.OrdinalIgnoreCase); + foreach (var plan in config.OrderBy(x => x.MaxApiCalls).Select(x => x.Clone())) + { + plansList.Add(plan); + plansById[plan.Id] = plan; + + if (!string.IsNullOrWhiteSpace(plan.YearlyId) && !string.IsNullOrWhiteSpace(plan.YearlyCosts)) + { + plansById[plan.YearlyId] = plan; + } + } } public IEnumerable GetAvailablePlans() diff --git a/src/Squidex.Infrastructure/EventSourcing/Grains/OrleansEventNotifier.cs b/src/Squidex.Infrastructure/EventSourcing/Grains/OrleansEventNotifier.cs index 0e97746e4..17900a901 100644 --- a/src/Squidex.Infrastructure/EventSourcing/Grains/OrleansEventNotifier.cs +++ b/src/Squidex.Infrastructure/EventSourcing/Grains/OrleansEventNotifier.cs @@ -10,26 +10,24 @@ using Orleans; namespace Squidex.Infrastructure.EventSourcing.Grains { - public sealed class OrleansEventNotifier : IEventNotifier, IInitializable + public sealed class OrleansEventNotifier : IEventNotifier { private readonly IGrainFactory factory; - private IEventConsumerManagerGrain eventConsumerManagerGrain; + private readonly Lazy eventConsumerManagerGrain; public OrleansEventNotifier(IGrainFactory factory) { Guard.NotNull(factory, nameof(factory)); - this.factory = factory; - } - - public void Initialize() - { - eventConsumerManagerGrain = factory.GetGrain("Default"); + eventConsumerManagerGrain = new Lazy(() => + { + return factory.GetGrain("Default"); + }); } public void NotifyEventsStored(string streamName) { - eventConsumerManagerGrain?.ActivateAsync(streamName); + eventConsumerManagerGrain.Value.ActivateAsync(streamName); } public IDisposable Subscribe(Action handler) diff --git a/src/Squidex.Infrastructure/Orleans/Bootstrap.cs b/src/Squidex.Infrastructure/Orleans/Bootstrap.cs index 79abd18b9..cf1f03145 100644 --- a/src/Squidex.Infrastructure/Orleans/Bootstrap.cs +++ b/src/Squidex.Infrastructure/Orleans/Bootstrap.cs @@ -14,6 +14,7 @@ namespace Squidex.Infrastructure.Orleans { public sealed class Bootstrap : IStartupTask where T : IBackgroundGrain { + private const int NumTries = 10; private readonly IGrainFactory grainFactory; public Bootstrap(IGrainFactory grainFactory) @@ -23,11 +24,26 @@ namespace Squidex.Infrastructure.Orleans this.grainFactory = grainFactory; } - public Task Execute(CancellationToken cancellationToken) + public async Task Execute(CancellationToken cancellationToken) { - var grain = grainFactory.GetGrain("Default"); + for (var i = 1; i <= NumTries; i++) + { + try + { + var grain = grainFactory.GetGrain("Default"); - return grain.ActivateAsync(); + await grain.ActivateAsync(); + + return; + } + catch (OrleansMessageRejectionException) + { + if (i == NumTries) + { + throw; + } + } + } } } } diff --git a/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs b/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs index b0d406c9b..0b93c9043 100644 --- a/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs +++ b/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs @@ -55,11 +55,12 @@ namespace Squidex.Areas.Api.Controllers.Plans public IActionResult GetPlans(string app) { var planId = appPlansProvider.GetPlanForApp(App).Id; + var plans = appPlansProvider.GetAvailablePlans().Select(x => SimpleMapper.Map(x, new PlanDto())).ToList(); var response = new AppPlansDto { CurrentPlanId = planId, - Plans = appPlansProvider.GetAvailablePlans().Select(x => SimpleMapper.Map(x, new PlanDto())).ToList(), + Plans = plans, PlanOwner = App.Plan?.Owner.Identifier, HasPortal = appPlansBillingManager.HasPortal }; diff --git a/src/Squidex/Areas/Api/Controllers/Plans/Models/PlanDto.cs b/src/Squidex/Areas/Api/Controllers/Plans/Models/PlanDto.cs index 59adb8595..7c330ad23 100644 --- a/src/Squidex/Areas/Api/Controllers/Plans/Models/PlanDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Plans/Models/PlanDto.cs @@ -5,6 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.ComponentModel.DataAnnotations; + namespace Squidex.Areas.Api.Controllers.Plans.Models { public sealed class PlanDto @@ -12,18 +14,31 @@ namespace Squidex.Areas.Api.Controllers.Plans.Models /// /// The id of the plan. /// + [Required] public string Id { get; set; } /// /// The name of the plan. /// + [Required] public string Name { get; set; } /// /// The monthly costs of the plan. /// + [Required] public string Costs { get; set; } + /// + /// The yearly costs of the plan. + /// + public string YearlyCosts { get; set; } + + /// + /// The yearly id of the plan. + /// + public string YearlyId { get; set; } + /// /// The maximum number of API calls. /// diff --git a/src/Squidex/Config/Domain/EventStoreServices.cs b/src/Squidex/Config/Domain/EventStoreServices.cs index baa744514..8f3c76ff3 100644 --- a/src/Squidex/Config/Domain/EventStoreServices.cs +++ b/src/Squidex/Config/Domain/EventStoreServices.cs @@ -53,8 +53,7 @@ namespace Squidex.Config.Domain }); services.AddSingletonAs() - .As() - .As(); + .As(); services.AddSingletonAs() .As(); diff --git a/src/Squidex/app/features/settings/pages/plans/plans-page.component.html b/src/Squidex/app/features/settings/pages/plans/plans-page.component.html index 0d29550a5..a376cece1 100644 --- a/src/Squidex/app/features/settings/pages/plans/plans-page.component.html +++ b/src/Squidex/app/features/settings/pages/plans/plans-page.component.html @@ -32,24 +32,25 @@
-
+

{{plan.name}}

{{plan.costs}}
- + Per Month
-
- {{plan.maxApiCalls | sqxKNumber}} API Calls -
-
- {{plan.maxAssetSize | sqxFileSize}} Storage +
+
+ {{plan.maxApiCalls | sqxKNumber}} API Calls +
+
+ {{plan.maxAssetSize | sqxFileSize}} Storage +
+
+ {{plan.maxContributors}} Contributors +
-
- {{plan.maxContributors}} Contributors -
-
-
+ @@ -58,6 +59,21 @@ Change
+
diff --git a/src/Squidex/app/features/settings/pages/plans/plans-page.component.scss b/src/Squidex/app/features/settings/pages/plans/plans-page.component.scss index 9c75fef9a..6079c4b72 100644 --- a/src/Squidex/app/features/settings/pages/plans/plans-page.component.scss +++ b/src/Squidex/app/features/settings/pages/plans/plans-page.component.scss @@ -8,12 +8,10 @@ margin: .5rem; } - &-header { - border-bottom: 1px solid $color-border; - } - &-price { color: $color-theme-blue; + margin-top: 0; + margin-bottom: 0; } &-selected { @@ -21,10 +19,20 @@ } &-fact { - line-height: 2rem; + line-height: 1.8rem; + } + + .btn { + margin-top: 1rem; } } +.card-footer, +.card-header, +.card-body { + padding: 1rem; +} + .empty { margin: 1.25rem; margin-top: 6.25rem; diff --git a/src/Squidex/app/shared/services/plans.service.spec.ts b/src/Squidex/app/shared/services/plans.service.spec.ts index 1d9d8e4ed..4202b4112 100644 --- a/src/Squidex/app/shared/services/plans.service.spec.ts +++ b/src/Squidex/app/shared/services/plans.service.spec.ts @@ -62,6 +62,8 @@ describe('PlansService', () => { id: 'free', name: 'Free', costs: '14 €', + yearlyId: 'free_yearly', + yearlyCosts: '12 €', maxApiCalls: 1000, maxAssetSize: 1500, maxContributors: 2500 @@ -70,6 +72,8 @@ describe('PlansService', () => { id: 'prof', name: 'Prof', costs: '18 €', + yearlyId: 'prof_yearly', + yearlyCosts: '16 €', maxApiCalls: 4000, maxAssetSize: 5500, maxContributors: 6500 @@ -87,8 +91,8 @@ describe('PlansService', () => { '456', true, [ - new PlanDto('free', 'Free', '14 €', 1000, 1500, 2500), - new PlanDto('prof', 'Prof', '18 €', 4000, 5500, 6500) + new PlanDto('free', 'Free', '14 €', 'free_yearly', '12 €', 1000, 1500, 2500), + new PlanDto('prof', 'Prof', '18 €', 'prof_yearly', '16 €', 4000, 5500, 6500) ], new Version('2') )); diff --git a/src/Squidex/app/shared/services/plans.service.ts b/src/Squidex/app/shared/services/plans.service.ts index 287e56831..7063eebf2 100644 --- a/src/Squidex/app/shared/services/plans.service.ts +++ b/src/Squidex/app/shared/services/plans.service.ts @@ -44,6 +44,8 @@ export class PlanDto { public readonly id: string, public readonly name: string, public readonly costs: string, + public readonly yearlyId: string, + public readonly yearlyCosts: string, public readonly maxApiCalls: number, public readonly maxAssetSize: number, public readonly maxContributors: number @@ -92,6 +94,8 @@ export class PlansService { item.id, item.name, item.costs, + item.yearlyId, + item.yearlyCosts, item.maxApiCalls, item.maxAssetSize, item.maxContributors); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Billing/ConfigAppLimitsProviderTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Billing/ConfigAppLimitsProviderTests.cs index 99dd68dab..059164011 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Billing/ConfigAppLimitsProviderTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Billing/ConfigAppLimitsProviderTests.cs @@ -41,7 +41,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.Billing Name = "Basic", MaxApiCalls = 150000, MaxAssetSize = 1024 * 1024 * 2, - MaxContributors = 5 + MaxContributors = 5, + YearlyCosts = "100€", + YearlyId = "basic_yearly" }; private static readonly ConfigAppLimitsPlan[] Plans = { BasicPlan, FreePlan }; @@ -76,6 +78,16 @@ namespace Squidex.Domain.Apps.Entities.Apps.Billing 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] public void Should_smallest_plan_if_none_fits() { From d946304f1e4203c5c6336938524cd1c8b20c392f Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sun, 8 Apr 2018 16:02:59 +0200 Subject: [PATCH 6/6] PR fixed --- .../Orleans/Bootstrap.cs | 2 +- .../Grains/OrleansEventNotifierTests.cs | 1 - .../Orleans/BootstrapTests.cs | 39 +++++++++++++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/Squidex.Infrastructure/Orleans/Bootstrap.cs b/src/Squidex.Infrastructure/Orleans/Bootstrap.cs index cf1f03145..254d31b49 100644 --- a/src/Squidex.Infrastructure/Orleans/Bootstrap.cs +++ b/src/Squidex.Infrastructure/Orleans/Bootstrap.cs @@ -36,7 +36,7 @@ namespace Squidex.Infrastructure.Orleans return; } - catch (OrleansMessageRejectionException) + catch (OrleansException) { if (i == NumTries) { diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/OrleansEventNotifierTests.cs b/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/OrleansEventNotifierTests.cs index 536e8829f..4c3e54e11 100644 --- a/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/OrleansEventNotifierTests.cs +++ b/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/OrleansEventNotifierTests.cs @@ -29,7 +29,6 @@ namespace Squidex.Infrastructure.EventSourcing.Grains [Fact] public void Should_wakeup_manager_with_stream_name() { - sut.Initialize(); sut.NotifyEventsStored("my-stream"); A.CallTo(() => manager.ActivateAsync("my-stream")) diff --git a/tests/Squidex.Infrastructure.Tests/Orleans/BootstrapTests.cs b/tests/Squidex.Infrastructure.Tests/Orleans/BootstrapTests.cs index 153a9d276..037aab083 100644 --- a/tests/Squidex.Infrastructure.Tests/Orleans/BootstrapTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Orleans/BootstrapTests.cs @@ -6,10 +6,13 @@ // All rights reserved. // ========================================================================== +using System; using System.Threading; using System.Threading.Tasks; using FakeItEasy; using Orleans; +using Orleans.Runtime; +using Squidex.Infrastructure.Tasks; using Xunit; namespace Squidex.Infrastructure.Orleans @@ -37,5 +40,41 @@ namespace Squidex.Infrastructure.Orleans A.CallTo(() => grain.ActivateAsync()) .MustHaveHappened(); } + + [Fact] + public async Task Should_fail_on_non_rejection_exception() + { + A.CallTo(() => grain.ActivateAsync()) + .Throws(new InvalidOperationException()); + + await Assert.ThrowsAsync(() => 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(() => sut.Execute(CancellationToken.None)); + + A.CallTo(() => grain.ActivateAsync()) + .MustHaveHappened(Repeated.Exactly.Times(10)); + } } }