diff --git a/src/Squidex.Web/Extensions.cs b/src/Squidex.Web/Extensions.cs index b7f7594bf..4ab57d830 100644 --- a/src/Squidex.Web/Extensions.cs +++ b/src/Squidex.Web/Extensions.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Security.Claims; using Squidex.Infrastructure.Security; @@ -40,5 +41,12 @@ namespace Squidex.Web return (null, null); } + + public static bool IsUser(this ApiController controller, string userId) + { + var subject = controller.User.OpenIdSubject(); + + return string.Equals(subject, userId, StringComparison.OrdinalIgnoreCase); + } } } diff --git a/src/Squidex.Web/PermissionExtensions.cs b/src/Squidex.Web/PermissionExtensions.cs new file mode 100644 index 000000000..6dc7d0610 --- /dev/null +++ b/src/Squidex.Web/PermissionExtensions.cs @@ -0,0 +1,77 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.AspNetCore.Http; +using Squidex.Infrastructure.Security; +using Squidex.Shared; +using Squidex.Shared.Identity; + +namespace Squidex.Web +{ + public static class PermissionExtensions + { + private sealed class PermissionFeature + { + public PermissionSet Permissions { get; } + + public PermissionFeature(PermissionSet permissions) + { + Permissions = permissions; + } + } + + public static PermissionSet GetPermissions(this HttpContext httpContext) + { + var feature = httpContext.Features.Get(); + + if (feature == null) + { + feature = new PermissionFeature(httpContext.User.Permissions()); + + httpContext.Features.Set(feature); + } + + return feature.Permissions; + } + + public static bool HasPermission(this HttpContext httpContext, Permission permission) + { + return httpContext.GetPermissions().Includes(permission); + } + + public static bool HasPermission(this HttpContext httpContext, string id, string app = "*", string schema = "*") + { + return httpContext.GetPermissions().Includes(Permissions.ForApp(id, app, schema)); + } + + public static bool HasPermission(this ApiController controller, Permission permission) + { + return controller.HttpContext.GetPermissions().Includes(permission); + } + + public static bool HasPermission(this ApiController controller, string id, string app = "*", string schema = "*") + { + if (app == "*") + { + if (controller.RouteData.Values.TryGetValue("app", out var value) && value is string s) + { + app = s; + } + } + + if (schema == "*") + { + if (controller.RouteData.Values.TryGetValue("name", out var value) && value is string s) + { + schema = s; + } + } + + return controller.HttpContext.GetPermissions().Includes(Permissions.ForApp(id, app, schema)); + } + } +} diff --git a/src/Squidex.Web/Resource.cs b/src/Squidex.Web/Resource.cs new file mode 100644 index 000000000..b62a5f361 --- /dev/null +++ b/src/Squidex.Web/Resource.cs @@ -0,0 +1,52 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Newtonsoft.Json; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Net.Http; + +namespace Squidex.Web +{ + public abstract class Resource + { + [JsonProperty("_links")] + [Required] + [Display(Description = "The links.")] + public Dictionary Links { get; } = new Dictionary(); + + public void AddSelfLink(string href) + { + AddGetLink("self", href); + } + + public void AddGetLink(string rel, string href) + { + AddLink(rel, HttpMethod.Get, href); + } + + public void AddPostLink(string rel, string href) + { + AddLink(rel, HttpMethod.Post, href); + } + + public void AddPutLink(string rel, string href) + { + AddLink(rel, HttpMethod.Put, href); + } + + public void AddDeleteLink(string rel, string href) + { + AddLink(rel, HttpMethod.Delete, href); + } + + public void AddLink(string rel, HttpMethod method, string href) + { + Links[rel] = new ResourceLink { Href = href, Method = method }; + } + } +} diff --git a/src/Squidex.Web/ResourceLink.cs b/src/Squidex.Web/ResourceLink.cs new file mode 100644 index 000000000..48627ac3a --- /dev/null +++ b/src/Squidex.Web/ResourceLink.cs @@ -0,0 +1,23 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.ComponentModel.DataAnnotations; +using System.Net.Http; + +namespace Squidex.Web +{ + public class ResourceLink + { + [Required] + [Display(Description = "The link url.")] + public string Href { get; set; } + + [Required] + [Display(Description = "The link method.")] + public HttpMethod Method { get; set; } + } +} diff --git a/src/Squidex.Web/UrlHelperExtensions.cs b/src/Squidex.Web/UrlHelperExtensions.cs new file mode 100644 index 000000000..486d48a76 --- /dev/null +++ b/src/Squidex.Web/UrlHelperExtensions.cs @@ -0,0 +1,46 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.AspNetCore.Mvc; +using System; +using System.Linq.Expressions; +using System.Reflection; + +namespace Squidex.Web +{ + public static class UrlHelperExtensions + { + private static class NameOf + { + public static readonly string Controller; + + static NameOf() + { + const string suffix = "Controller"; + + var name = typeof(T).Name; + + if (name.EndsWith(suffix)) + { + name = name.Substring(0, name.Length - suffix.Length); + } + + Controller = name; + } + } + + public static string Url(this IUrlHelper urlHelper, Func action, object values = null) where T : Controller + { + return urlHelper.Action(action(null), NameOf.Controller, values); + } + + public static string Url(this Controller controller, Func action, object values = null) where T : Controller + { + return controller.Url.Url(action, values); + } + } +} diff --git a/src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs b/src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs index 8a2a5a2d4..2481f73c8 100644 --- a/src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs @@ -8,12 +8,18 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.Security; using Squidex.Shared.Users; +using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Users.Models { - public sealed class UserDto + public sealed class UserDto : Resource { + private static readonly Permission LockPermission = new Permission(Shared.Permissions.AdminUsersLock); + private static readonly Permission UnlockPermission = new Permission(Shared.Permissions.AdminUsersUnlock); + private static readonly Permission UpdatePermission = new Permission(Shared.Permissions.AdminUsersUpdate); + /// /// The id of the user. /// @@ -44,11 +50,50 @@ namespace Squidex.Areas.Api.Controllers.Users.Models [Required] public IEnumerable Permissions { get; set; } - public static UserDto FromUser(IUser user) + public static UserDto FromUser(IUser user, ApiController controller) + { + var userPermssions = user.Permissions().ToIds(); + var userName = user.DisplayName(); + + var result = SimpleMapper.Map(user, new UserDto { DisplayName = userName, Permissions = userPermssions }); + + return CreateLinks(result, controller); + } + + private static UserDto CreateLinks(UserDto result, ApiController controller) { - var permissions = user.Permissions().ToIds(); + var values = new { id = result.Id }; + + if (controller is UserManagementController) + { + result.AddSelfLink(controller.Url(c => nameof(c.GetUser), values)); + } + else + { + result.AddSelfLink(controller.Url(c => nameof(c.GetUser), values)); + } + + if (!controller.IsUser(result.Id)) + { + if (controller.HasPermission(LockPermission) && !result.IsLocked) + { + result.AddPutLink("lock", controller.Url(c => nameof(c.LockUser), values)); + } + + if (controller.HasPermission(UnlockPermission) && result.IsLocked) + { + result.AddPutLink("unlock", controller.Url(c => nameof(c.UnlockUser), values)); + } + } + + if (controller.HasPermission(UpdatePermission)) + { + result.AddPutLink("update", controller.Url(c => nameof(c.PutUser), values)); + } + + result.AddGetLink("picture", controller.Url(c => nameof(c.GetUserPicture), values)); - return SimpleMapper.Map(user, new UserDto { DisplayName = user.DisplayName(), Permissions = permissions }); + return result; } } } diff --git a/src/Squidex/Areas/Api/Controllers/Users/Models/UsersDto.cs b/src/Squidex/Areas/Api/Controllers/Users/Models/UsersDto.cs index 2d83ff47d..e866c8af4 100644 --- a/src/Squidex/Areas/Api/Controllers/Users/Models/UsersDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Users/Models/UsersDto.cs @@ -5,10 +5,19 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Collections.Generic; +using System.Linq; +using Squidex.Domain.Users; +using Squidex.Infrastructure.Security; +using Squidex.Shared; +using Squidex.Web; + namespace Squidex.Areas.Api.Controllers.Users.Models { - public sealed class UsersDto + public sealed class UsersDto : Resource { + private static readonly Permission CreatePermissions = new Permission(Permissions.AdminUsersCreate); + /// /// The total number of users. /// @@ -18,5 +27,28 @@ namespace Squidex.Areas.Api.Controllers.Users.Models /// The users. /// public UserDto[] Items { get; set; } + + public static UsersDto FromResults(IEnumerable items, long total, ApiController controller) + { + var result = new UsersDto + { + Total = total, + Items = items.Select(x => UserDto.FromUser(x, controller)).ToArray() + }; + + return CreateLinks(result, controller); + } + + private static UsersDto CreateLinks(UsersDto result, ApiController controller) + { + result.AddSelfLink(controller.Url(c => nameof(c.GetUsers))); + + if (controller.HasPermission(CreatePermissions)) + { + result.AddPostLink("create", controller.Url(c => nameof(c.PostUser))); + } + + return result; + } } } diff --git a/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs b/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs index 7d547be14..16862daab 100644 --- a/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs +++ b/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs @@ -5,8 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; -using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; @@ -14,7 +12,6 @@ using Squidex.Areas.Api.Controllers.Users.Models; using Squidex.Domain.Users; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Security; using Squidex.Shared; using Squidex.Web; @@ -43,11 +40,7 @@ namespace Squidex.Areas.Api.Controllers.Users await Task.WhenAll(taskForItems, taskForCount); - var response = new UsersDto - { - Total = taskForCount.Result, - Items = taskForItems.Result.Select(UserDto.FromUser).ToArray() - }; + var response = UsersDto.FromResults(taskForItems.Result, taskForCount.Result, this); return Ok(response); } @@ -64,7 +57,7 @@ namespace Squidex.Areas.Api.Controllers.Users return NotFound(); } - var response = UserDto.FromUser(entity); + var response = UserDto.FromUser(entity, this); return Ok(response); } @@ -96,7 +89,7 @@ namespace Squidex.Areas.Api.Controllers.Users [ApiPermission(Permissions.AdminUsersLock)] public async Task LockUser(string id) { - if (IsSelf(id)) + if (this.IsUser(id)) { throw new ValidationException("Locking user failed.", new ValidationError("You cannot lock yourself.")); } @@ -111,7 +104,7 @@ namespace Squidex.Areas.Api.Controllers.Users [ApiPermission(Permissions.AdminUsersUnlock)] public async Task UnlockUser(string id) { - if (IsSelf(id)) + if (this.IsUser(id)) { throw new ValidationException("Unlocking user failed.", new ValidationError("You cannot unlock yourself.")); } @@ -120,12 +113,5 @@ namespace Squidex.Areas.Api.Controllers.Users return NoContent(); } - - private bool IsSelf(string id) - { - var subject = User.OpenIdSubject(); - - return string.Equals(subject, id, StringComparison.OrdinalIgnoreCase); - } } } diff --git a/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs b/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs index 36c556adb..5311ff642 100644 --- a/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs +++ b/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs @@ -76,7 +76,7 @@ namespace Squidex.Areas.Api.Controllers.Users { var entities = await userResolver.QueryByEmailAsync(query); - var models = entities.Where(x => !x.IsHidden()).Select(UserDto.FromUser).ToArray(); + var models = entities.Where(x => !x.IsHidden()).Select(x => UserDto.FromUser(x, this)).ToArray(); return Ok(models); } @@ -110,7 +110,7 @@ namespace Squidex.Areas.Api.Controllers.Users if (entity != null) { - var response = UserDto.FromUser(entity); + var response = UserDto.FromUser(entity, this); return Ok(response); } diff --git a/src/Squidex/app/features/administration/pages/users/user-page.component.html b/src/Squidex/app/features/administration/pages/users/user-page.component.html index b9d1bdb51..d23721240 100644 --- a/src/Squidex/app/features/administration/pages/users/user-page.component.html +++ b/src/Squidex/app/features/administration/pages/users/user-page.component.html @@ -50,7 +50,7 @@ -
+
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 0c7e1efc2..9a7e9b71a 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 @@ -26,7 +26,7 @@ import { export class UserPageComponent extends ResourceOwner implements OnInit { public canUpdate = false; - public user?: { user: UserDto, isCurrentUser: boolean }; + public user?: UserDto; public userForm = new UserForm(this.formBuilder); constructor( @@ -45,7 +45,7 @@ export class UserPageComponent extends ResourceOwner implements OnInit { this.user = selectedUser!; if (selectedUser) { - this.userForm.load(selectedUser.user); + this.userForm.load(selectedUser); } })); } @@ -55,7 +55,7 @@ export class UserPageComponent extends ResourceOwner implements OnInit { if (value) { if (this.user) { - this.usersState.update(this.user.user, value) + this.usersState.update(this.user, value) .subscribe(() => { this.userForm.submitCompleted(); }, 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 index 223f584bf..167ee3eb9 100644 --- a/src/Squidex/app/features/administration/pages/users/users-page.component.html +++ b/src/Squidex/app/features/administration/pages/users/users-page.component.html @@ -48,32 +48,24 @@
- - + + diff --git a/src/Squidex/app/features/administration/services/users.service.ts b/src/Squidex/app/features/administration/services/users.service.ts index a657eee38..b42fe46a2 100644 --- a/src/Squidex/app/features/administration/services/users.service.ts +++ b/src/Squidex/app/features/administration/services/users.service.ts @@ -14,12 +14,19 @@ import { ApiUrlConfig, Model, pretifyError, - ResultSet + Resource, + ResourceLinks, + ResultSet, + withLinks } from '@app/shared'; -export class UsersDto extends ResultSet {} +export class UsersDto extends ResultSet { + public _links: ResourceLinks; +} export class UserDto extends Model { + public _links: ResourceLinks; + constructor( public readonly id: string, public readonly email: string, @@ -60,17 +67,19 @@ export class UsersService { public getUsers(take: number, skip: number, query?: string): Observable { const url = this.apiUrl.buildUrl(`api/user-management?take=${take}&skip=${skip}&query=${query || ''}`); - return this.http.get<{ total: number, items: any[] }>(url).pipe( + return this.http.get<{ total: number, items: any[] } & Resource>(url).pipe( map(body => { const users = body.items.map(item => - new UserDto( - item.id, - item.email, - item.displayName, - item.permissions, - item.isLocked)); - - return new UsersDto(body.total, users); + withLinks( + new UserDto( + item.id, + item.email, + item.displayName, + item.permissions, + item.isLocked), + item)); + + return withLinks(new UsersDto(body.total, users), body); }), pretifyError('Failed to load users. Please reload.')); } diff --git a/src/Squidex/app/features/administration/state/users.state.spec.ts b/src/Squidex/app/features/administration/state/users.state.spec.ts index c2a912ecd..e735870a5 100644 --- a/src/Squidex/app/features/administration/state/users.state.spec.ts +++ b/src/Squidex/app/features/administration/state/users.state.spec.ts @@ -16,7 +16,7 @@ import { UsersService } from '@app/features/administration/internal'; -import { SnapshotUser, UsersState } from './users.state'; +import { UsersState } from './users.state'; describe('UsersState', () => { const oldUsers = [ @@ -26,21 +26,15 @@ describe('UsersState', () => { const newUser = new UserDto('id3', 'mail3@mail.de', 'name3', ['Permission3'], false); - let authService: IMock; let dialogs: IMock; let usersService: IMock; let usersState: UsersState; beforeEach(() => { - authService = Mock.ofType(); - - authService.setup(x => x.user) - .returns(() => { id: 'id2' }); - dialogs = Mock.ofType(); usersService = Mock.ofType(); - usersState = new UsersState(authService.object, dialogs.object, usersService.object); + usersState = new UsersState(dialogs.object, usersService.object); }); afterEach(() => { @@ -54,10 +48,7 @@ describe('UsersState', () => { usersState.load().subscribe(); - expect(usersState.snapshot.users.values).toEqual([ - { isCurrentUser: false, user: oldUsers[0] }, - { isCurrentUser: true, user: oldUsers[1] } - ]); + expect(usersState.snapshot.users.values).toEqual(oldUsers); expect(usersState.snapshot.usersPager.numberOfItems).toEqual(200); expect(usersState.snapshot.isLoaded).toBeTruthy(); @@ -91,7 +82,7 @@ describe('UsersState', () => { usersState.select('id1').subscribe(); usersState.load().subscribe(); - expect(usersState.snapshot.selectedUser).toEqual({ isCurrentUser: false, user: newUsers[0] }); + expect(usersState.snapshot.selectedUser).toEqual(newUsers[0]); }); it('should load next page and prev page when paging', () => { @@ -127,32 +118,32 @@ describe('UsersState', () => { }); it('should return user on select and not load when already loaded', () => { - let selectedUser: SnapshotUser; + let selectedUser: UserDto; usersState.select('id1').subscribe(x => { selectedUser = x!; }); - expect(selectedUser!.user).toEqual(oldUsers[0]); - expect(usersState.snapshot.selectedUser).toEqual({ isCurrentUser: false, user: oldUsers[0] }); + expect(selectedUser!).toEqual(oldUsers[0]); + expect(usersState.snapshot.selectedUser).toEqual(oldUsers[0]); }); it('should return user on select and load when not loaded', () => { usersService.setup(x => x.getUser('id3')) .returns(() => of(newUser)); - let selectedUser: SnapshotUser; + let selectedUser: UserDto; usersState.select('id3').subscribe(x => { selectedUser = x!; }); - expect(selectedUser!.user).toEqual(newUser); - expect(usersState.snapshot.selectedUser).toEqual({ isCurrentUser: false, user: newUser }); + expect(selectedUser!).toEqual(newUser); + expect(usersState.snapshot.selectedUser).toEqual(newUser); }); it('should return null on select when unselecting user', () => { - let selectedUser: SnapshotUser; + let selectedUser: UserDto; usersState.select(null).subscribe(x => { selectedUser = x!; @@ -166,7 +157,7 @@ describe('UsersState', () => { usersService.setup(x => x.getUser('unknown')) .returns(() => throwError({})).verifiable(); - let selectedUser: SnapshotUser; + let selectedUser: UserDto; usersState.select('unknown').subscribe(x => { selectedUser = x!; @@ -185,7 +176,7 @@ describe('UsersState', () => { const user_1 = usersState.snapshot.users.at(0); - expect(user_1.user.isLocked).toBeTruthy(); + expect(user_1.isLocked).toBeTruthy(); expect(user_1).toBe(usersState.snapshot.selectedUser!); }); @@ -198,7 +189,7 @@ describe('UsersState', () => { const user_1 = usersState.snapshot.users.at(1); - expect(user_1.user.isLocked).toBeFalsy(); + expect(user_1.isLocked).toBeFalsy(); expect(user_1).toBe(usersState.snapshot.selectedUser!); }); @@ -213,9 +204,9 @@ describe('UsersState', () => { const user_1 = usersState.snapshot.users.at(0); - expect(user_1.user.email).toEqual(request.email); - expect(user_1.user.displayName).toEqual(request.displayName); - expect(user_1.user.permissions).toEqual(request.permissions); + expect(user_1.email).toEqual(request.email); + expect(user_1.displayName).toEqual(request.displayName); + expect(user_1.permissions).toEqual(request.permissions); expect(user_1).toBe(usersState.snapshot.selectedUser!); }); @@ -227,11 +218,7 @@ describe('UsersState', () => { usersState.create(request).subscribe(); - expect(usersState.snapshot.users.values).toEqual([ - { isCurrentUser: false, user: newUser }, - { isCurrentUser: false, user: oldUsers[0] }, - { isCurrentUser: true, user: oldUsers[1] } - ]); + expect(usersState.snapshot.users.values).toEqual([newUser, ...oldUsers]); expect(usersState.snapshot.usersPager.numberOfItems).toBe(201); }); }); diff --git a/src/Squidex/app/features/administration/state/users.state.ts b/src/Squidex/app/features/administration/state/users.state.ts index 7de907b24..49c5163cc 100644 --- a/src/Squidex/app/features/administration/state/users.state.ts +++ b/src/Squidex/app/features/administration/state/users.state.ts @@ -7,12 +7,11 @@ import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; -import { catchError, distinctUntilChanged, map, tap } from 'rxjs/operators'; +import { catchError, distinctUntilChanged, map, switchMap, tap } from 'rxjs/operators'; import '@app/framework/utils/rxjs-extensions'; import { - AuthService, DialogService, ImmutableArray, Pager, @@ -27,14 +26,6 @@ import { UsersService } from './../services/users.service'; -export interface SnapshotUser { - // The user. - user: UserDto; - - // Indicates if the user is the current user. - isCurrentUser: boolean; -} - interface Snapshot { // The current users. users: UsersList; @@ -49,10 +40,10 @@ interface Snapshot { isLoaded?: boolean; // The selected user. - selectedUser?: SnapshotUser | null; + selectedUser?: UserDto | null; } -export type UsersList = ImmutableArray; +export type UsersList = ImmutableArray; export type UsersResult = { total: number, users: UsersList }; @Injectable() @@ -74,14 +65,13 @@ export class UsersState extends State { distinctUntilChanged()); constructor( - private readonly authState: AuthService, private readonly dialogs: DialogService, private readonly usersService: UsersService ) { super({ users: ImmutableArray.empty(), usersPager: new Pager(0) }); } - public select(id: string | null): Observable { + public select(id: string | null): Observable { return this.loadUser(id).pipe( tap(selectedUser => { this.next(s => ({ ...s, selectedUser })); @@ -94,13 +84,13 @@ export class UsersState extends State { return of(null); } - const found = this.snapshot.users.find(x => x.user.id === id); + const found = this.snapshot.users.find(x => x.id === id); if (found) { return of(found); } - return this.usersService.getUser(id).pipe(map(x => this.createUser(x)), catchError(() => of(null))); + return this.usersService.getUser(id).pipe(catchError(() => of(null))); } public load(isReload = false): Observable { @@ -125,12 +115,12 @@ export class UsersState extends State { this.next(s => { const usersPager = s.usersPager.setCount(total); - const users = ImmutableArray.of(items.map(x => this.createUser(x))); + const users = ImmutableArray.of(items); let selectedUser = s.selectedUser; if (selectedUser) { - selectedUser = users.find(x => x.user.id === selectedUser!.user.id) || selectedUser; + selectedUser = users.find(x => x.id === selectedUser!.id) || selectedUser; } return { ...s, users, usersPager, selectedUser, isLoaded: true }; @@ -143,7 +133,7 @@ export class UsersState extends State { return this.usersService.postUser(request).pipe( tap(created => { this.next(s => { - const users = s.users.pushFront(this.createUser(created)); + const users = s.users.pushFront(created); const usersPager = s.usersPager.incrementCount(); return { ...s, users, usersPager }; @@ -154,7 +144,7 @@ export class UsersState extends State { public update(user: UserDto, request: UpdateUserDto): Observable { return this.usersService.putUser(user.id, request).pipe( - map(() => update(user, request)), + switchMap(() => this.usersService.getUser(user.id)), tap(updated => { this.replaceUser(updated); }), @@ -163,7 +153,7 @@ export class UsersState extends State { public lock(user: UserDto): Observable { return this.usersService.lockUser(user.id).pipe( - map(() => setLocked(user, true)), + switchMap(() => this.usersService.getUser(user.id)), tap(updated => { this.replaceUser(updated); }), @@ -172,7 +162,7 @@ export class UsersState extends State { public unlock(user: UserDto): Observable { return this.usersService.unlockUser(user.id).pipe( - map(() => setLocked(user, false)), + switchMap(() => this.usersService.getUser(user.id)), tap(updated => { this.replaceUser(updated); }), @@ -199,30 +189,15 @@ export class UsersState extends State { private replaceUser(user: UserDto) { return this.next(s => { - const users = s.users.map(u => u.user.id === user.id ? this.createUser(user) : u); + const users = s.users.map(u => u.id === user.id ? user : u); const selectedUser = s.selectedUser && - s.selectedUser.user.id !== user.id ? + s.selectedUser.id !== user.id ? s.selectedUser : - users.find(x => x.user.id === user.id); + users.find(x => x.id === user.id); return { ...s, users, selectedUser }; }); } - - private get userId() { - return this.authState.user!.id; - } - - private createUser(user: UserDto): SnapshotUser { - return { user, isCurrentUser: user.id === this.userId }; - } -} - - -const update = (user: UserDto, request: UpdateUserDto) => - user.with(request); - -const setLocked = (user: UserDto, isLocked: boolean) => - user.with({ isLocked }); \ No newline at end of file +} \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/http/hateos.pipes.ts b/src/Squidex/app/framework/angular/http/hateos.pipes.ts new file mode 100644 index 000000000..979c95205 --- /dev/null +++ b/src/Squidex/app/framework/angular/http/hateos.pipes.ts @@ -0,0 +1,23 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +import { Resource } from '@app/framework/internal'; + +@Pipe({ + name: 'sqxHasLink', + pure: true +}) +export class HasLinkPipe implements PipeTransform { + public transform(value: Resource, rel: string) { + return value._links && !!value._links[rel]; + } +} + +@Pipe({ + name: 'sqxHasNoLink', + pure: true +}) +export class HasNoLinkPipe implements PipeTransform { + public transform(value: Resource, rel: string) { + return !value._links || !value._links[rel]; + } +} \ No newline at end of file diff --git a/src/Squidex/app/framework/declarations.ts b/src/Squidex/app/framework/declarations.ts index 7111dde33..5cf90253c 100644 --- a/src/Squidex/app/framework/declarations.ts +++ b/src/Squidex/app/framework/declarations.ts @@ -31,6 +31,7 @@ export * from './angular/forms/validators'; export * from './angular/http/caching.interceptor'; export * from './angular/http/loading.interceptor'; +export * from './angular/http/hateos.pipes'; export * from './angular/http/http-extensions'; export * from './angular/modals/dialog-renderer.component'; diff --git a/src/Squidex/app/framework/internal.ts b/src/Squidex/app/framework/internal.ts index 776e7df49..73b658619 100644 --- a/src/Squidex/app/framework/internal.ts +++ b/src/Squidex/app/framework/internal.ts @@ -23,6 +23,7 @@ export * from './utils/date-helper'; export * from './utils/date-time'; export * from './utils/duration'; export * from './utils/error'; +export * from './utils/hateos'; export * from './utils/interpolator'; export * from './utils/immutable-array'; export * from './utils/math-helper'; diff --git a/src/Squidex/app/framework/module.ts b/src/Squidex/app/framework/module.ts index 5921bf736..b18aad34b 100644 --- a/src/Squidex/app/framework/module.ts +++ b/src/Squidex/app/framework/module.ts @@ -42,6 +42,8 @@ import { FormHintComponent, FromNowPipe, FullDateTimePipe, + HasLinkPipe, + HasNoLinkPipe, HoverBackgroundDirective, IFrameEditorComponent, IgnoreScrollbarDirective, @@ -122,6 +124,8 @@ import { FormHintComponent, FromNowPipe, FullDateTimePipe, + HasLinkPipe, + HasNoLinkPipe, HoverBackgroundDirective, IFrameEditorComponent, IgnoreScrollbarDirective, @@ -188,6 +192,8 @@ import { FormsModule, FromNowPipe, FullDateTimePipe, + HasLinkPipe, + HasNoLinkPipe, HoverBackgroundDirective, IFrameEditorComponent, IgnoreScrollbarDirective, diff --git a/src/Squidex/app/framework/utils/hateos.ts b/src/Squidex/app/framework/utils/hateos.ts new file mode 100644 index 000000000..14e5357d8 --- /dev/null +++ b/src/Squidex/app/framework/utils/hateos.ts @@ -0,0 +1,25 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + + export interface Resource { + _links?: { [rel: string]: ResourceLink }; + } + + export type ResourceLinks = { [rel: string]: ResourceLink }; + export type ResourceLink = { href: string; method: ResourceMethod; }; + + export function withLinks(value: T, source: Resource) { + value._links = source._links; + + return value; + } + + export type ResourceMethod = + 'get' | + 'post' | + 'put' | + 'delete'; \ No newline at end of file
- + - {{userInfo.user.displayName}} + {{user.displayName}} - {{userInfo.user.email}} + {{user.email}} - - - - - - - - + +