From 7f2b7f75d9356268657d88e5bdeab8607c76126a Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sun, 19 Feb 2017 18:21:22 +0100 Subject: [PATCH] User management --- .../Contents/MongoContentRepository.cs | 2 +- .../Contents/Visitors/FindExtensions.cs | 14 +- .../History/MongoHistoryEventRepository.cs | 2 +- .../Users/MongoUserRepository.cs | 53 +- .../Repositories/IContentRepository.cs | 2 +- .../Repositories/IHistoryEventRepository.cs | 2 +- .../Schemas/Repositories/ISchemaRepository.cs | 4 +- .../Users/Repositories/IUserRepository.cs | 8 +- .../Config/Identity/IdentityServices.cs | 1 - src/Squidex/Config/Identity/IdentityUsage.cs | 1 - .../EventConsumersController.cs | 3 +- .../Api/History/HistoryController.cs | 2 +- .../Controllers/Api/Users/Models/UserDto.cs | 6 + .../Controllers/Api/Users/Models/UsersDto.cs | 23 + .../Api/Users/UserManagementController.cs | 90 ++ .../Controllers/Api/Users/UsersController.cs | 2 +- .../UI/Account/AccountController.cs | 25 +- .../administration-area.component.html | 7 +- .../administration-area.component.scss | 44 - .../features/administration/declarations.ts | 1 + .../app/features/administration/module.ts | 11 +- .../event-consumers-page.component.ts | 16 +- .../pages/users/users-page.component.html | 89 ++ .../pages/users/users-page.component.scss | 18 + .../pages/users/users-page.component.ts | 135 +++ .../pages/content/content-page.component.ts | 2 +- .../pages/contents/content-item.component.ts | 2 +- .../contents/contents-page.component.html | 6 +- .../contents/contents-page.component.scss | 19 - .../pages/contents/contents-page.component.ts | 3 +- .../pages/schemas/schemas-page.component.ts | 2 +- .../pages/dashboard-page.component.ts | 2 +- .../pages/schema/schema-page.component.ts | 2 +- .../pages/schemas/schemas-page.component.ts | 2 +- .../pages/clients/client.component.html | 4 +- .../pages/clients/clients-page.component.ts | 2 +- .../contributors-page.component.html | 2 +- .../contributors-page.component.scss | 15 - .../contributors-page.component.ts | 2 +- .../languages/languages-page.component.scss | 5 - .../languages/languages-page.component.ts | 2 +- .../shared/components/app-component-base.ts | 67 +- .../app/shared/components/component-base.ts | 77 ++ .../shared/components/history.component.ts | 4 +- .../language-selector.component.scss | 4 - src/Squidex/app/shared/declarations.ts | 1 + src/Squidex/app/shared/module.ts | 2 + .../shared/services/app-clients.service.ts | 1 - .../services/app-contributors.service.ts | 1 - .../shared/services/app-languages.service.ts | 3 +- .../app/shared/services/history.service.ts | 3 +- .../app/shared/services/languages.service.ts | 3 +- .../services/users-provider.service.spec.ts | 12 +- .../shared/services/users-provider.service.ts | 4 +- .../app/shared/services/users.service.spec.ts | 151 ++- .../app/shared/services/users.service.ts | 71 +- .../shell/pages/app/app-area.component.scss | 2 + .../shell/pages/app/left-menu.component.scss | 46 +- src/Squidex/app/theme/_lists.scss | 24 + src/Squidex/app/theme/_panels.scss | 47 + .../app/theme/icomoon/demo-files/demo.css | 158 +++ .../app/theme/icomoon/demo-files/demo.js | 30 + src/Squidex/app/theme/icomoon/demo.html | 988 ++++++++++++++++++ .../app/theme/icomoon/fonts/icomoon.eot | Bin 12772 -> 13164 bytes .../app/theme/icomoon/fonts/icomoon.svg | 5 +- .../app/theme/icomoon/fonts/icomoon.ttf | Bin 12608 -> 13000 bytes .../app/theme/icomoon/fonts/icomoon.woff | Bin 12684 -> 13076 bytes src/Squidex/app/theme/icomoon/icons/user.svg | 24 +- src/Squidex/app/theme/icomoon/selection.json | 598 ++++++----- src/Squidex/app/theme/icomoon/style.css | 61 +- 70 files changed, 2437 insertions(+), 583 deletions(-) create mode 100644 src/Squidex/Controllers/Api/Users/Models/UsersDto.cs create mode 100644 src/Squidex/Controllers/Api/Users/UserManagementController.cs create mode 100644 src/Squidex/app/features/administration/pages/users/users-page.component.html create mode 100644 src/Squidex/app/features/administration/pages/users/users-page.component.scss create mode 100644 src/Squidex/app/features/administration/pages/users/users-page.component.ts create mode 100644 src/Squidex/app/shared/components/component-base.ts create mode 100644 src/Squidex/app/theme/icomoon/demo-files/demo.css create mode 100644 src/Squidex/app/theme/icomoon/demo-files/demo.js create mode 100644 src/Squidex/app/theme/icomoon/demo.html diff --git a/src/Squidex.Read.MongoDb/Contents/MongoContentRepository.cs b/src/Squidex.Read.MongoDb/Contents/MongoContentRepository.cs index 652f33e48..f6f64ed39 100644 --- a/src/Squidex.Read.MongoDb/Contents/MongoContentRepository.cs +++ b/src/Squidex.Read.MongoDb/Contents/MongoContentRepository.cs @@ -46,7 +46,7 @@ namespace Squidex.Read.MongoDb.Contents this.schemaProvider = schemaProvider; } - public async Task> QueryAsync(Guid schemaId, bool nonPublished, string odataQuery, HashSet languages) + public async Task> QueryAsync(Guid schemaId, bool nonPublished, string odataQuery, HashSet languages) { List result = null; diff --git a/src/Squidex.Read.MongoDb/Contents/Visitors/FindExtensions.cs b/src/Squidex.Read.MongoDb/Contents/Visitors/FindExtensions.cs index 8b69dc737..49427da42 100644 --- a/src/Squidex.Read.MongoDb/Contents/Visitors/FindExtensions.cs +++ b/src/Squidex.Read.MongoDb/Contents/Visitors/FindExtensions.cs @@ -8,6 +8,7 @@ using System.Collections.Generic; using Microsoft.OData.Core.UriParser; +using MongoDB.Bson; using MongoDB.Driver; using Squidex.Core.Schemas; @@ -69,7 +70,18 @@ namespace Squidex.Read.MongoDb.Contents.Visitors filters.Add(filter); } - return Filter.And(filters); + if (filters.Count > 1) + { + return Filter.And(filters); + } + else if (filters.Count == 1) + { + return filters[0]; + } + else + { + return new BsonDocument(); + } } } } diff --git a/src/Squidex.Read.MongoDb/History/MongoHistoryEventRepository.cs b/src/Squidex.Read.MongoDb/History/MongoHistoryEventRepository.cs index c4f2adfa1..0b57267b1 100644 --- a/src/Squidex.Read.MongoDb/History/MongoHistoryEventRepository.cs +++ b/src/Squidex.Read.MongoDb/History/MongoHistoryEventRepository.cs @@ -59,7 +59,7 @@ namespace Squidex.Read.MongoDb.History collection.Indexes.CreateOneAsync(IndexKeys.Ascending(x => x.Created), new CreateIndexOptions { ExpireAfter = TimeSpan.FromDays(365) })); } - public async Task> QueryEventsByChannel(Guid appId, string channelPrefix, int count) + public async Task> QueryByChannelAsync(Guid appId, string channelPrefix, int count) { var entities = await Collection.Find(x => x.AppId == appId && x.Channel == channelPrefix) diff --git a/src/Squidex.Read.MongoDb/Users/MongoUserRepository.cs b/src/Squidex.Read.MongoDb/Users/MongoUserRepository.cs index 71d6bf499..d282cf336 100644 --- a/src/Squidex.Read.MongoDb/Users/MongoUserRepository.cs +++ b/src/Squidex.Read.MongoDb/Users/MongoUserRepository.cs @@ -6,6 +6,7 @@ // All rights reserved. // ========================================================================== +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -15,6 +16,8 @@ using Squidex.Infrastructure; using Squidex.Read.Users; using Squidex.Read.Users.Repositories; +// ReSharper disable InvertIf + namespace Squidex.Read.MongoDb.Users { public sealed class MongoUserRepository : IUserRepository @@ -28,11 +31,18 @@ namespace Squidex.Read.MongoDb.Users this.userManager = userManager; } - public Task> QueryUsersByQuery(string query) + public Task> QueryByEmailAsync(string email, int take = 10, int skip = 0) { - var users = userManager.Users.Where(x => x.NormalizedEmail.Contains(query.ToUpper())).Take(10).ToList(); + var users = QueryUsers(email).Skip(skip).Take(take).ToList(); - return Task.FromResult(users.Select(x => (IUserEntity)new MongoUserEntity(x)).ToList()); + return Task.FromResult>(users.Select(x => (IUserEntity)new MongoUserEntity(x)).ToList()); + } + + public Task CountAsync(string email = null) + { + var count = QueryUsers(email).LongCount(); + + return Task.FromResult(count); } public async Task FindUserByIdAsync(string id) @@ -42,11 +52,42 @@ namespace Squidex.Read.MongoDb.Users return user != null ? new MongoUserEntity(user) : null; } - public Task CountAsync() + public async Task LockAsync(string id) { - var count = userManager.Users.LongCount(); + var user = await userManager.FindByIdAsync(id); - return Task.FromResult(count); + if (user == null) + { + throw new DomainObjectNotFoundException(id, typeof(IdentityUser)); + } + + await userManager.SetLockoutEndDateAsync(user, DateTimeOffset.UtcNow.AddYears(100)); + } + + public async Task UnlockAsync(string id) + { + var user = await userManager.FindByIdAsync(id); + + if (user == null) + { + throw new DomainObjectNotFoundException(id, typeof(IdentityUser)); + } + + await userManager.SetLockoutEndDateAsync(user, null); + } + + private IQueryable QueryUsers(string email = null) + { + var result = userManager.Users; + + if (!string.IsNullOrWhiteSpace(email)) + { + var upperEmail = email.ToUpperInvariant(); + + result = userManager.Users.Where(x => x.NormalizedEmail.Contains(upperEmail)); + } + + return result; } } } diff --git a/src/Squidex.Read/Contents/Repositories/IContentRepository.cs b/src/Squidex.Read/Contents/Repositories/IContentRepository.cs index 914b3e843..cdde53612 100644 --- a/src/Squidex.Read/Contents/Repositories/IContentRepository.cs +++ b/src/Squidex.Read/Contents/Repositories/IContentRepository.cs @@ -15,7 +15,7 @@ namespace Squidex.Read.Contents.Repositories { public interface IContentRepository { - Task> QueryAsync(Guid schemaId, bool nonPublished, string odataQuery, HashSet languages); + Task> QueryAsync(Guid schemaId, bool nonPublished, string odataQuery, HashSet languages); Task CountAsync(Guid schemaId, bool nonPublished, string odataQuery, HashSet languages); diff --git a/src/Squidex.Read/History/Repositories/IHistoryEventRepository.cs b/src/Squidex.Read/History/Repositories/IHistoryEventRepository.cs index db0c64063..e15426d99 100644 --- a/src/Squidex.Read/History/Repositories/IHistoryEventRepository.cs +++ b/src/Squidex.Read/History/Repositories/IHistoryEventRepository.cs @@ -14,6 +14,6 @@ namespace Squidex.Read.History.Repositories { public interface IHistoryEventRepository { - Task> QueryEventsByChannel(Guid appId, string channelPrefix, int count); + Task> QueryByChannelAsync(Guid appId, string channelPrefix, int count); } } diff --git a/src/Squidex.Read/Schemas/Repositories/ISchemaRepository.cs b/src/Squidex.Read/Schemas/Repositories/ISchemaRepository.cs index b7a73a821..ddb4c743e 100644 --- a/src/Squidex.Read/Schemas/Repositories/ISchemaRepository.cs +++ b/src/Squidex.Read/Schemas/Repositories/ISchemaRepository.cs @@ -21,10 +21,10 @@ namespace Squidex.Read.Schemas.Repositories Task> QueryAllWithSchemaAsync(Guid appId); - Task FindSchemaIdAsync(Guid appId, string name); - Task FindSchemaAsync(Guid appId, string name); Task FindSchemaAsync(Guid schemaId); + + Task FindSchemaIdAsync(Guid appId, string name); } } diff --git a/src/Squidex.Read/Users/Repositories/IUserRepository.cs b/src/Squidex.Read/Users/Repositories/IUserRepository.cs index 6974858bd..94e2ee15c 100644 --- a/src/Squidex.Read/Users/Repositories/IUserRepository.cs +++ b/src/Squidex.Read/Users/Repositories/IUserRepository.cs @@ -13,10 +13,14 @@ namespace Squidex.Read.Users.Repositories { public interface IUserRepository { - Task> QueryUsersByQuery(string query); + Task> QueryByEmailAsync(string email = null, int take = 10, int skip = 0); Task FindUserByIdAsync(string id); - Task CountAsync(); + Task LockAsync(string id); + + Task UnlockAsync(string id); + + Task CountAsync(string email = null); } } diff --git a/src/Squidex/Config/Identity/IdentityServices.cs b/src/Squidex/Config/Identity/IdentityServices.cs index 9ea33fab2..0b8c18797 100644 --- a/src/Squidex/Config/Identity/IdentityServices.cs +++ b/src/Squidex/Config/Identity/IdentityServices.cs @@ -21,7 +21,6 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Squidex.Core.Identity; using Squidex.Infrastructure; -using Squidex.Infrastructure.Security; using StackExchange.Redis; namespace Squidex.Config.Identity diff --git a/src/Squidex/Config/Identity/IdentityUsage.cs b/src/Squidex/Config/Identity/IdentityUsage.cs index 688d84e96..65b67d542 100644 --- a/src/Squidex/Config/Identity/IdentityUsage.cs +++ b/src/Squidex/Config/Identity/IdentityUsage.cs @@ -19,7 +19,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Newtonsoft.Json.Linq; using Squidex.Core.Identity; -using Squidex.Infrastructure.Security; // ReSharper disable InvertIf diff --git a/src/Squidex/Controllers/Api/EventConsumers/EventConsumersController.cs b/src/Squidex/Controllers/Api/EventConsumers/EventConsumersController.cs index 1f3303292..b191a8d46 100644 --- a/src/Squidex/Controllers/Api/EventConsumers/EventConsumersController.cs +++ b/src/Squidex/Controllers/Api/EventConsumers/EventConsumersController.cs @@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using NSwag.Annotations; using Squidex.Controllers.Api.EventConsumers.Models; +using Squidex.Core.Identity; using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.Reflection; using Squidex.Pipeline; @@ -19,7 +20,7 @@ using Squidex.Pipeline; namespace Squidex.Controllers.Api.EventConsumers { [ApiExceptionFilter] - [Authorize(Roles = "administrator")] + [Authorize(Roles = SquidexRoles.Administrator)] [SwaggerIgnore] public sealed class EventConsumersController : Controller { diff --git a/src/Squidex/Controllers/Api/History/HistoryController.cs b/src/Squidex/Controllers/Api/History/HistoryController.cs index a254675e7..abefb5aee 100644 --- a/src/Squidex/Controllers/Api/History/HistoryController.cs +++ b/src/Squidex/Controllers/Api/History/HistoryController.cs @@ -61,7 +61,7 @@ namespace Squidex.Controllers.Api.History return NotFound(); } - var schemas = await historyEventRepository.QueryEventsByChannel(entity.Id, channel, 100); + var schemas = await historyEventRepository.QueryByChannelAsync(entity.Id, channel, 100); var response = schemas.Select(x => SimpleMapper.Map(x, new HistoryEventDto())).ToList(); diff --git a/src/Squidex/Controllers/Api/Users/Models/UserDto.cs b/src/Squidex/Controllers/Api/Users/Models/UserDto.cs index aac99720f..cc2778101 100644 --- a/src/Squidex/Controllers/Api/Users/Models/UserDto.cs +++ b/src/Squidex/Controllers/Api/Users/Models/UserDto.cs @@ -35,5 +35,11 @@ namespace Squidex.Controllers.Api.Users.Models /// [Required] public string DisplayName { get; set; } + + /// + /// Determines if the user is locked. + /// + [Required] + public bool IsLocked { get; set; } } } diff --git a/src/Squidex/Controllers/Api/Users/Models/UsersDto.cs b/src/Squidex/Controllers/Api/Users/Models/UsersDto.cs new file mode 100644 index 000000000..fc0ca93fd --- /dev/null +++ b/src/Squidex/Controllers/Api/Users/Models/UsersDto.cs @@ -0,0 +1,23 @@ +// ========================================================================== +// UsersDto.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Controllers.Api.Users.Models +{ + public class UsersDto + { + /// + /// The total number of users. + /// + public long Total { get; set; } + + /// + /// The users. + /// + public UserDto[] Items { get; set; } + } +} diff --git a/src/Squidex/Controllers/Api/Users/UserManagementController.cs b/src/Squidex/Controllers/Api/Users/UserManagementController.cs new file mode 100644 index 000000000..aa03c55a4 --- /dev/null +++ b/src/Squidex/Controllers/Api/Users/UserManagementController.cs @@ -0,0 +1,90 @@ +// ========================================================================== +// UserManagementController.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using NSwag.Annotations; +using Squidex.Controllers.Api.Users.Models; +using Squidex.Core.Identity; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.Security; +using Squidex.Pipeline; +using Squidex.Read.Users.Repositories; + +namespace Squidex.Controllers.Api.Users +{ + [ApiExceptionFilter] + [Authorize(Roles = SquidexRoles.Administrator)] + [SwaggerIgnore] + public class UserManagementController : Controller + { + private readonly IUserRepository userRepository; + + public UserManagementController(IUserRepository userRepository) + { + this.userRepository = userRepository; + } + + [HttpGet] + [Route("user-management")] + public async Task GetUsers([FromQuery] string query = null, [FromQuery] int skip = 0, [FromQuery] int take = 10) + { + var taskForUsers = userRepository.QueryByEmailAsync(query, take, skip); + var taskForCount = userRepository.CountAsync(query); + + await Task.WhenAll(taskForUsers, taskForCount); + + var model = new UsersDto + { + Total = taskForCount.Result, + Items = taskForUsers.Result.Select(x => SimpleMapper.Map(x, new UserDto())).ToArray() + }; + + return Ok(model); + } + + [HttpPut] + [Route("user-management/{id}/lock/")] + public async Task Lock(string id) + { + if (IsSelf(id)) + { + throw new ValidationException("Locking user failed.", new ValidationError("You cannot lock yourself.")); + } + + await userRepository.LockAsync(id); + + return NoContent(); + } + + [HttpPut] + [Route("user-management/{id}/unlock/")] + public async Task Unlock(string id) + { + if (IsSelf(id)) + { + throw new ValidationException("Unlocking user failed.", new ValidationError("You cannot unlock yourself.")); + } + + await userRepository.UnlockAsync(id); + + return NoContent(); + } + + private bool IsSelf(string id) + { + var subject = User.OpenIdSubject(); + + return string.Equals(subject, id, StringComparison.OrdinalIgnoreCase); + } + } +} diff --git a/src/Squidex/Controllers/Api/Users/UsersController.cs b/src/Squidex/Controllers/Api/Users/UsersController.cs index e21db617d..0159a51df 100644 --- a/src/Squidex/Controllers/Api/Users/UsersController.cs +++ b/src/Squidex/Controllers/Api/Users/UsersController.cs @@ -48,7 +48,7 @@ namespace Squidex.Controllers.Api.Users [ProducesResponseType(typeof(UserDto[]), 200)] public async Task GetUsers(string query) { - var entities = await userRepository.QueryUsersByQuery(query ?? string.Empty); + var entities = await userRepository.QueryByEmailAsync(query ?? string.Empty); var response = entities.Select(x => SimpleMapper.Map(x, new UserDto())).ToList(); diff --git a/src/Squidex/Controllers/UI/Account/AccountController.cs b/src/Squidex/Controllers/UI/Account/AccountController.cs index 346b12ad6..85d43ba7e 100644 --- a/src/Squidex/Controllers/UI/Account/AccountController.cs +++ b/src/Squidex/Controllers/UI/Account/AccountController.cs @@ -8,11 +8,11 @@ using System; using System.Linq; +using System.Runtime.CompilerServices; using System.Security.Claims; using System.Text; using System.Threading.Tasks; using IdentityServer4.Services; -using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.MongoDB; using Microsoft.AspNetCore.Mvc; @@ -22,7 +22,6 @@ using Microsoft.Extensions.Options; using Squidex.Config; using Squidex.Config.Identity; using Squidex.Core.Identity; -using Squidex.Read.Users.Repositories; // ReSharper disable InvertIf // ReSharper disable RedundantIfElseBlock @@ -38,7 +37,6 @@ namespace Squidex.Controllers.UI.Account private readonly UserManager userManager; private readonly IOptions identityOptions; private readonly IOptions urlOptions; - private readonly IUserRepository userRepository; private readonly ILogger logger; private readonly IIdentityServerInteractionService interactions; @@ -47,14 +45,12 @@ namespace Squidex.Controllers.UI.Account UserManager userManager, IOptions identityOptions, IOptions urlOptions, - IUserRepository userRepository, ILogger logger, IIdentityServerInteractionService interactions) { this.logger = logger; this.urlOptions = urlOptions; this.userManager = userManager; - this.userRepository = userRepository; this.interactions = interactions; this.identityOptions = identityOptions; this.signInManager = signInManager; @@ -119,8 +115,7 @@ namespace Squidex.Controllers.UI.Account { var providers = signInManager.GetExternalAuthenticationSchemes() - .Select(x => new ExternalProvider(x.AuthenticationScheme, x.DisplayName)) - .ToList(); + .Select(x => new ExternalProvider(x.AuthenticationScheme, x.DisplayName)).ToList(); return View(new LoginVM { ExternalProviders = providers, ReturnUrl = returnUrl }); } @@ -160,7 +155,7 @@ namespace Squidex.Controllers.UI.Account { var user = CreateUser(externalLogin); - var isFirst = await userRepository.CountAsync() == 0; + var isFirst = userManager.Users.LongCount() == 0; isLoggedIn = await AddUserAsync(user) && @@ -184,16 +179,14 @@ namespace Squidex.Controllers.UI.Account } } - private async Task AddLoginAsync(IdentityUser user, UserLoginInfo externalLogin) + private Task AddLoginAsync(IdentityUser user, UserLoginInfo externalLogin) { - var result = await userManager.AddLoginAsync(user, externalLogin); - - return result.Succeeded; + return MakeIdentityOperation(() => userManager.AddLoginAsync(user, externalLogin)); } private Task AddUserAsync(IdentityUser user) { - return MakeIdentityOperation("LoginAsync", () => userManager.CreateAsync(user)); + return MakeIdentityOperation(() => userManager.CreateAsync(user)); } private async Task LoginAsync(UserLoginInfo externalLogin) @@ -210,7 +203,7 @@ namespace Squidex.Controllers.UI.Account return Task.FromResult(false); } - return MakeIdentityOperation("LockAsync", () => userManager.SetLockoutEndDateAsync(user, DateTimeOffset.UtcNow.AddYears(100))); + return MakeIdentityOperation(() => userManager.SetLockoutEndDateAsync(user, DateTimeOffset.UtcNow.AddYears(100))); } private Task MakeAdminAsync(IdentityUser user, bool isFirst) @@ -220,7 +213,7 @@ namespace Squidex.Controllers.UI.Account return Task.FromResult(false); } - return MakeIdentityOperation("LockAsync", () => userManager.AddToRoleAsync(user, SquidexRoles.Administrator)); + return MakeIdentityOperation(() => userManager.AddToRoleAsync(user, SquidexRoles.Administrator)); } private static IdentityUser CreateUser(ExternalLoginInfo externalLogin) @@ -237,7 +230,7 @@ namespace Squidex.Controllers.UI.Account return user; } - private async Task MakeIdentityOperation(string operationName, Func> action) + private async Task MakeIdentityOperation(Func> action, [CallerMemberName] string operationName = null) { try { diff --git a/src/Squidex/app/features/administration/administration-area.component.html b/src/Squidex/app/features/administration/administration-area.component.html index 0890e6dfd..957acf31d 100644 --- a/src/Squidex/app/features/administration/administration-area.component.html +++ b/src/Squidex/app/features/administration/administration-area.component.html @@ -2,7 +2,12 @@ diff --git a/src/Squidex/app/features/administration/administration-area.component.scss b/src/Squidex/app/features/administration/administration-area.component.scss index 8c3d26f9d..23f99dbeb 100644 --- a/src/Squidex/app/features/administration/administration-area.component.scss +++ b/src/Squidex/app/features/administration/administration-area.component.scss @@ -1,46 +1,2 @@ @import '_vars'; @import '_mixins'; - -.sidebar { - @include fixed($size-navbar-height, auto, 0, 0); - @include box-shadow-colored(2px, 0, 0, $color-dark1-border2); - min-width: $size-sidebar-width; - max-width: $size-sidebar-width; - border-right: 1px solid $color-dark1-border1; - background: $color-dark1-background; - z-index: 100; -} - -.nav { - &-icon { - font-size: 2rem; - } - - &-text { - font-size: .9rem; - } - - &-link { - & { - @include transition(color .3s ease); - padding: 1.25rem; - display: block; - text-align: center; - text-decoration: none; - color: $color-dark1-foreground; - } - - &:hover, - &.active { - color: $color-dark1-focus-foreground; - - .nav-icon { - color: $color-theme-blue; - } - } - - &.active { - background: $color-dark1-active-background; - } - } -} \ No newline at end of file diff --git a/src/Squidex/app/features/administration/declarations.ts b/src/Squidex/app/features/administration/declarations.ts index 278b1a192..c28ce2b53 100644 --- a/src/Squidex/app/features/administration/declarations.ts +++ b/src/Squidex/app/features/administration/declarations.ts @@ -6,5 +6,6 @@ */ export * from './pages/event-consumers/event-consumers-page.component'; +export * from './pages/users/users-page.component'; export * from './administration-area.component'; \ No newline at end of file diff --git a/src/Squidex/app/features/administration/module.ts b/src/Squidex/app/features/administration/module.ts index ac6885a07..4b8911e70 100644 --- a/src/Squidex/app/features/administration/module.ts +++ b/src/Squidex/app/features/administration/module.ts @@ -15,7 +15,8 @@ import { import { AdministrationAreaComponent, - EventConsumersPage + EventConsumersPageComponent, + UsersPageComponent } from './declarations'; const routes: Routes = [ @@ -27,7 +28,10 @@ const routes: Routes = [ path: '', children: [{ path: 'event-consumers', - component: EventConsumersPage + component: EventConsumersPageComponent + }, { + path: 'users', + component: UsersPageComponent }] } ] @@ -42,7 +46,8 @@ const routes: Routes = [ ], declarations: [ AdministrationAreaComponent, - EventConsumersPage + EventConsumersPageComponent, + UsersPageComponent ] }) export class SqxFeatureAdministrationModule { } \ No newline at end of file diff --git a/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.ts b/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.ts index d6006fd2b..749f5c32d 100644 --- a/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.ts +++ b/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.ts @@ -9,11 +9,14 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { Observable, Subscription } from 'rxjs'; import { + ComponentBase, EventConsumerDto, EventConsumersService, fadeAnimation, ImmutableArray, - ModalView + ModalView, + NotificationService, + UsersProviderService } from 'shared'; @Component({ @@ -24,16 +27,17 @@ import { fadeAnimation ] }) -export class EventConsumersPage implements OnInit, OnDestroy { +export class EventConsumersPageComponent extends ComponentBase implements OnInit, OnDestroy { private subscription: Subscription; public eventConsumerErrorDialog = new ModalView(); public eventConsumerError = ''; public eventConsumers = ImmutableArray.empty(); - constructor( + constructor(notifications: NotificationService, users: UsersProviderService, private readonly eventConsumersService: EventConsumersService ) { + super(notifications, users); } public ngOnInit() { @@ -59,6 +63,8 @@ export class EventConsumersPage implements OnInit, OnDestroy { return e; } }); + }, error => { + this.notifyError(error); }); } @@ -72,6 +78,8 @@ export class EventConsumersPage implements OnInit, OnDestroy { return e; } }); + }, error => { + this.notifyError(error); }); } @@ -85,6 +93,8 @@ export class EventConsumersPage implements OnInit, OnDestroy { return e; } }); + }, error => { + this.notifyError(error); }); } diff --git a/src/Squidex/app/features/administration/pages/users/users-page.component.html b/src/Squidex/app/features/administration/pages/users/users-page.component.html new file mode 100644 index 000000000..137223632 --- /dev/null +++ b/src/Squidex/app/features/administration/pages/users/users-page.component.html @@ -0,0 +1,89 @@ + + + +
+
+
+
+ +
+
+ +

Users

+
+ + + + +
+ +
+
+ + + + + + + + + + + + + + + + + + + + +
+   + + Name + + Email + + Actions +
+ +
+ +
+
+
+
\ No newline at end of file diff --git a/src/Squidex/app/features/administration/pages/users/users-page.component.scss b/src/Squidex/app/features/administration/pages/users/users-page.component.scss new file mode 100644 index 000000000..a02bf784d --- /dev/null +++ b/src/Squidex/app/features/administration/pages/users/users-page.component.scss @@ -0,0 +1,18 @@ +@import '_vars'; +@import '_mixins'; + +.col-right { + text-align: right; +} + +.user { + &-name, + &-email { + @include truncate; + } + + &-email { + font-style: italic; + font-size: .8rem; + } +} \ No newline at end of file diff --git a/src/Squidex/app/features/administration/pages/users/users-page.component.ts b/src/Squidex/app/features/administration/pages/users/users-page.component.ts new file mode 100644 index 000000000..7f5c44dc4 --- /dev/null +++ b/src/Squidex/app/features/administration/pages/users/users-page.component.ts @@ -0,0 +1,135 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { Component, OnInit } from '@angular/core'; +import { FormControl } from '@angular/forms'; + +import { + AuthService, + ComponentBase, + ImmutableArray, + NotificationService, + UserDto, + UserManagementService, + UsersProviderService +} from 'shared'; + +@Component({ + selector: 'sqx-users-page', + styleUrls: ['./users-page.component.scss'], + templateUrl: './users-page.component.html' +}) +export class UsersPageComponent extends ComponentBase implements OnInit { + public currentUserId: string; + + public usersItems = ImmutableArray.empty(); + public usersTotal = 0; + + public pageSize = 10; + + public canGoNext = false; + public canGoPrev = false; + + public itemFirst = 0; + public itemLast = 0; + + public currentPage = 0; + public currentQuery = ''; + + public usersFilter = new FormControl(); + + constructor(notifications: NotificationService, users: UsersProviderService, + private readonly userManagementService: UserManagementService, + private readonly authService: AuthService + ) { + super(notifications, users); + } + + public ngOnInit() { + this.currentUserId = this.authService.user!.id; + + this.load(); + } + + public search() { + this.currentPage = 0; + this.currentQuery = this.usersFilter.value; + + this.load(); + } + + private load() { + this.userManagementService.getUsers(this.pageSize, this.currentPage * this.pageSize, this.currentQuery) + .subscribe(dtos => { + this.usersItems = ImmutableArray.of(dtos.items); + this.usersTotal = dtos.total; + + this.updatePaging(); + }, error => { + this.notifyError(error); + }); + } + + public lock(id: string) { + this.userManagementService.lockUser(id) + .subscribe(() => { + this.usersItems = this.usersItems.map(u => { + if (u.id === id) { + return new UserDto(u.id, u.email, u.displayName, u.pictureUrl, true); + } else { + return u; + } + }); + }, error => { + this.notifyError(error); + }); + } + + public unlock(id: string) { + this.userManagementService.unlockUser(id) + .subscribe(() => { + this.usersItems = this.usersItems.map(u => { + if (u.id === id) { + return new UserDto(u.id, u.email, u.displayName, u.pictureUrl, false); + } else { + return u; + } + }); + }, error => { + this.notifyError(error); + }); + } + + public goNext() { + if (this.canGoNext) { + this.currentPage++; + + this.updatePaging(); + this.load(); + } + } + + public goPrev() { + if (this.canGoPrev) { + this.currentPage--; + + this.updatePaging(); + this.load(); + } + } + + private updatePaging() { + const totalPages = Math.ceil(this.usersTotal / this.pageSize); + + this.itemFirst = this.currentPage * this.pageSize + 1; + this.itemLast = Math.min(this.usersTotal, (this.currentPage + 1) * this.pageSize); + + this.canGoNext = this.currentPage < totalPages - 1; + this.canGoPrev = this.currentPage > 0; + } +} + diff --git a/src/Squidex/app/features/content/pages/content/content-page.component.ts b/src/Squidex/app/features/content/pages/content/content-page.component.ts index e44090c5f..bec6f4169 100644 --- a/src/Squidex/app/features/content/pages/content/content-page.component.ts +++ b/src/Squidex/app/features/content/pages/content/content-page.component.ts @@ -56,7 +56,7 @@ export class ContentPageComponent extends AppComponentBase implements OnDestroy, private readonly router: Router, private readonly messageBus: MessageBus ) { - super(apps, notifications, users); + super(notifications, users, apps); } public ngOnDestroy() { diff --git a/src/Squidex/app/features/content/pages/contents/content-item.component.ts b/src/Squidex/app/features/content/pages/contents/content-item.component.ts index 03232c0ea..b7abf920d 100644 --- a/src/Squidex/app/features/content/pages/contents/content-item.component.ts +++ b/src/Squidex/app/features/content/pages/contents/content-item.component.ts @@ -55,7 +55,7 @@ export class ContentItemComponent extends AppComponentBase implements OnInit { public values: any[] = []; constructor(apps: AppsStoreService, notifications: NotificationService, users: UsersProviderService) { - super(apps, notifications, users); + super(notifications, users, apps); } public ngOnInit() { diff --git a/src/Squidex/app/features/content/pages/contents/contents-page.component.html b/src/Squidex/app/features/content/pages/contents/contents-page.component.html index 842b8a575..631f75a1e 100644 --- a/src/Squidex/app/features/content/pages/contents/contents-page.component.html +++ b/src/Squidex/app/features/content/pages/contents/contents-page.component.html @@ -10,9 +10,9 @@ - +

{{schema|displayName}} Contents

@@ -67,7 +67,7 @@