Browse Source

User Management improved

pull/65/head
Sebastian Stehle 9 years ago
parent
commit
eb4b40b1bc
  1. 6
      src/Squidex.Read.MongoDb/Users/MongoUserStore.cs
  2. 2
      src/Squidex.Read/Users/IUserResolver.cs
  3. 4
      src/Squidex.Read/Users/UserManagerExtensions.cs
  4. 2
      src/Squidex.Write/Apps/AppCommandHandler.cs
  5. 1
      src/Squidex/Config/Domain/StoreMongoDbModule.cs
  6. 21
      src/Squidex/Controllers/Api/Users/Models/UserCreatedDto.cs
  7. 29
      src/Squidex/Controllers/Api/Users/UserManagementController.cs
  8. 1
      src/Squidex/app/features/administration/declarations.ts
  9. 18
      src/Squidex/app/features/administration/module.ts
  10. 25
      src/Squidex/app/features/administration/pages/users/messages.ts
  11. 65
      src/Squidex/app/features/administration/pages/users/user-page.component.html
  12. 6
      src/Squidex/app/features/administration/pages/users/user-page.component.scss
  13. 157
      src/Squidex/app/features/administration/pages/users/user-page.component.ts
  14. 10
      src/Squidex/app/features/administration/pages/users/users-page.component.html
  15. 37
      src/Squidex/app/features/administration/pages/users/users-page.component.ts
  16. 20
      src/Squidex/app/features/content/pages/content/content-page.component.ts
  17. 2
      src/Squidex/app/features/content/pages/contents/contents-page.component.html
  18. 7
      src/Squidex/app/features/content/pages/contents/contents-page.component.ts
  19. 8
      src/Squidex/app/features/schemas/pages/schema/schema-edit-form.component.ts
  20. 4
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts
  21. 12
      src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts
  22. 4
      src/Squidex/app/features/settings/pages/languages/languages-page.component.ts
  23. 7
      src/Squidex/app/shared/components/app-form.component.ts
  24. 7
      src/Squidex/app/shared/components/asset.component.ts
  25. 1
      src/Squidex/app/shared/declarations-base.ts
  26. 85
      src/Squidex/app/shared/guards/resolve-user.guard.spec.ts
  27. 62
      src/Squidex/app/shared/guards/resolve-user.guard.ts
  28. 3
      src/Squidex/app/shared/module.ts
  29. 28
      src/Squidex/app/shared/services/users.service.spec.ts
  30. 58
      src/Squidex/app/shared/services/users.service.ts
  31. 15
      src/Squidex/app/theme/_lists.scss
  32. 8
      tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs

6
src/Squidex.Read.MongoDb/Users/MongoUserStore.cs

@ -31,6 +31,7 @@ namespace Squidex.Read.MongoDb.Users
IUserLockoutStore<IUser>, IUserLockoutStore<IUser>,
IUserAuthenticationTokenStore<IUser>, IUserAuthenticationTokenStore<IUser>,
IUserFactory, IUserFactory,
IUserResolver,
IQueryableUserStore<IUser> IQueryableUserStore<IUser>
{ {
private readonly UserStore<WrappedIdentityUser> innerStore; private readonly UserStore<WrappedIdentityUser> innerStore;
@ -60,6 +61,11 @@ namespace Squidex.Read.MongoDb.Users
return new WrappedIdentityUser { Email = email, UserName = email }; return new WrappedIdentityUser { Email = email, UserName = email };
} }
public async Task<IUser> FindByIdAsync(string userId)
{
return await innerStore.FindByIdAsync(userId, CancellationToken.None);
}
public async Task<IUser> FindByIdAsync(string userId, CancellationToken cancellationToken) public async Task<IUser> FindByIdAsync(string userId, CancellationToken cancellationToken)
{ {
return await innerStore.FindByIdAsync(userId, cancellationToken); return await innerStore.FindByIdAsync(userId, cancellationToken);

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

@ -12,6 +12,6 @@ namespace Squidex.Read.Users
{ {
public interface IUserResolver public interface IUserResolver
{ {
Task<IUser> FindById(string id); Task<IUser> FindByIdAsync(string id);
} }
} }

4
src/Squidex.Read/Users/UserManagerExtensions.cs

@ -50,7 +50,7 @@ namespace Squidex.Read.Users
return result; return result;
} }
public static async Task<string> CreateAsync(this UserManager<IUser> userManager, IUserFactory factory, string email, string displayName, string password) public static async Task<IUser> CreateAsync(this UserManager<IUser> userManager, IUserFactory factory, string email, string displayName, string password)
{ {
var user = factory.Create(email); var user = factory.Create(email);
@ -64,7 +64,7 @@ namespace Squidex.Read.Users
await DoChecked(() => userManager.AddPasswordAsync(user, password), "Cannot create user."); await DoChecked(() => userManager.AddPasswordAsync(user, password), "Cannot create user.");
} }
return user.Id; return user;
} }
public static async Task UpdateAsync(this UserManager<IUser> userManager, string id, string email, string displayName, string password) public static async Task UpdateAsync(this UserManager<IUser> userManager, string id, string email, string displayName, string password)

2
src/Squidex.Write/Apps/AppCommandHandler.cs

@ -69,7 +69,7 @@ namespace Squidex.Write.Apps
protected async Task On(AssignContributor command, CommandContext context) protected async Task On(AssignContributor command, CommandContext context)
{ {
if (await userResolver.FindById(command.ContributorId) == null) if (await userResolver.FindByIdAsync(command.ContributorId) == null)
{ {
var error = var error =
new ValidationError("Cannot find contributor the contributor", new ValidationError("Cannot find contributor the contributor",

1
src/Squidex/Config/Domain/StoreMongoDbModule.cs

@ -88,6 +88,7 @@ namespace Squidex.Config.Domain
.WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseRegistration)) .WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseRegistration))
.As<IUserStore<IUser>>() .As<IUserStore<IUser>>()
.As<IUserFactory>() .As<IUserFactory>()
.As<IUserResolver>()
.SingleInstance(); .SingleInstance();
builder.RegisterType<MongoRoleStore>() builder.RegisterType<MongoRoleStore>()

21
src/Squidex/Controllers/Api/Users/Models/UserCreatedDto.cs

@ -0,0 +1,21 @@
// ==========================================================================
// UserCreatedDto.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.ComponentModel.DataAnnotations;
namespace Squidex.Controllers.Api.Users.Models
{
public class UserCreatedDto
{
[Required]
public string Id { get; set; }
[Required]
public string PictureUrl { get; set; }
}
}

29
src/Squidex/Controllers/Api/Users/UserManagementController.cs

@ -54,14 +54,31 @@ namespace Squidex.Controllers.Api.Users
return Ok(response); return Ok(response);
} }
[HttpGet]
[Route("user-management/{id}")]
[ApiCosts(0)]
public async Task<IActionResult> GetUser(string id)
{
var entity = await userManager.FindByIdAsync(id);
if (entity == null)
{
return NotFound();
}
var response = SimpleMapper.Map(entity, new UserDto { DisplayName = entity.DisplayName(), PictureUrl = entity.PictureUrl() });
return Ok(response);
}
[HttpPost] [HttpPost]
[Route("user-management")] [Route("user-management")]
[ApiCosts(0)] [ApiCosts(0)]
public async Task<IActionResult> Create([FromBody] CreateUserDto request) public async Task<IActionResult> PostUser([FromBody] CreateUserDto request)
{ {
var id = await userManager.CreateAsync(userFactory, request.Email, request.DisplayName, request.Password); var user = await userManager.CreateAsync(userFactory, request.Email, request.DisplayName, request.Password);
var response = new EntityCreatedDto { Id = id }; var response = new UserCreatedDto { Id = user.Id, PictureUrl = user.PictureUrl() };
return Ok(response); return Ok(response);
} }
@ -69,7 +86,7 @@ namespace Squidex.Controllers.Api.Users
[HttpPut] [HttpPut]
[Route("user-management/{id}")] [Route("user-management/{id}")]
[ApiCosts(0)] [ApiCosts(0)]
public async Task<IActionResult> Update(string id, [FromBody] UpdateUserDto request) public async Task<IActionResult> PutUser(string id, [FromBody] UpdateUserDto request)
{ {
await userManager.UpdateAsync(id, request.Email, request.DisplayName, request.Password); await userManager.UpdateAsync(id, request.Email, request.DisplayName, request.Password);
@ -79,7 +96,7 @@ namespace Squidex.Controllers.Api.Users
[HttpPut] [HttpPut]
[Route("user-management/{id}/lock/")] [Route("user-management/{id}/lock/")]
[ApiCosts(0)] [ApiCosts(0)]
public async Task<IActionResult> Lock(string id) public async Task<IActionResult> LockUser(string id)
{ {
if (IsSelf(id)) if (IsSelf(id))
{ {
@ -94,7 +111,7 @@ namespace Squidex.Controllers.Api.Users
[HttpPut] [HttpPut]
[Route("user-management/{id}/unlock/")] [Route("user-management/{id}/unlock/")]
[ApiCosts(0)] [ApiCosts(0)]
public async Task<IActionResult> Unlock(string id) public async Task<IActionResult> UnlockUser(string id)
{ {
if (IsSelf(id)) if (IsSelf(id))
{ {

1
src/Squidex/app/features/administration/declarations.ts

@ -6,6 +6,7 @@
*/ */
export * from './pages/event-consumers/event-consumers-page.component'; export * from './pages/event-consumers/event-consumers-page.component';
export * from './pages/users/user-page.component';
export * from './pages/users/users-page.component'; export * from './pages/users/users-page.component';
export * from './administration-area.component'; export * from './administration-area.component';

18
src/Squidex/app/features/administration/module.ts

@ -9,6 +9,7 @@ import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
import { import {
ResolveUserGuard,
SqxFrameworkModule, SqxFrameworkModule,
SqxSharedModule SqxSharedModule
} from 'shared'; } from 'shared';
@ -16,6 +17,7 @@ import {
import { import {
AdministrationAreaComponent, AdministrationAreaComponent,
EventConsumersPageComponent, EventConsumersPageComponent,
UserPageComponent,
UsersPageComponent UsersPageComponent
} from './declarations'; } from './declarations';
@ -33,7 +35,20 @@ const routes: Routes = [
}, },
{ {
path: 'users', path: 'users',
component: UsersPageComponent component: UsersPageComponent,
children: [
{
path: 'new',
component: UserPageComponent
},
{
path: ':userId',
component: UserPageComponent,
resolve: {
user: ResolveUserGuard
}
}
]
} }
] ]
} }
@ -50,6 +65,7 @@ const routes: Routes = [
declarations: [ declarations: [
AdministrationAreaComponent, AdministrationAreaComponent,
EventConsumersPageComponent, EventConsumersPageComponent,
UserPageComponent,
UsersPageComponent UsersPageComponent
] ]
}) })

25
src/Squidex/app/features/administration/pages/users/messages.ts

@ -0,0 +1,25 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
export class UserCreated {
constructor(
public readonly id: string,
public readonly email: string,
public readonly displayName: string,
public readonly pictureUrl: string
) {
}
}
export class UserUpdated {
constructor(
public readonly id: string,
public readonly email: string,
public readonly displayName: string
) {
}
}

65
src/Squidex/app/features/administration/pages/users/user-page.component.html

@ -0,0 +1,65 @@
<sqx-title message="User Management"></sqx-title>
<form [formGroup]="userForm" (ngSubmit)="save()">
<sqx-panel panelWidth="26rem">
<div class="panel-header">
<div class="panel-title-row">
<div class="float-right">
<button type="submit" class="btn btn-primary" title="CTRL + S">
Save
</button>
</div>
<sqx-shortcut keys="ctrl+s" (trigger)="saveAndPusaveblish()"></sqx-shortcut>
<h3 class="panel-title" *ngIf="isNewMode">
New User
</h3>
<h3 class="panel-title" *ngIf="!isNewMode">
Edit User
</h3>
</div>
<sqx-shortcut keys="ctrl+s" (trigger)="saveAndPublish()"></sqx-shortcut>
<a class="panel-close" sqxParentLink>
<i class="icon-close"></i>
</a>
</div>
<div class="panel-main">
<div class="panel-content panel-content-blank">
<div class="form-group">
<label for="email">Email</label>
<sqx-control-errors for="email" [submitted]="userFormSubmitted"></sqx-control-errors>
<input type="email" class="form-control" id="email" maxlength="100" formControlName="email" />
</div>
<div class="form-group">
<label for="displayName">Display Name</label>
<sqx-control-errors for="displayName" [submitted]="userFormSubmitted"></sqx-control-errors>
<input type="text" class="form-control" id="displayName" maxlength="100" formControlName="displayName" />
</div>
<div class="form-group form-group-password">
<label for="password">Password</label>
<sqx-control-errors for="password" [submitted]="userFormSubmitted"></sqx-control-errors>
<input type="password" class="form-control" id="password" maxlength="100" formControlName="password" />
</div>
<div class="form-group">
<label for="passwordConfirm">Confirm Password</label>
<sqx-control-errors for="passwordConfirm" [submitted]="userFormSubmitted"></sqx-control-errors>
<input type="password" class="form-control" id="passwordConfirm" maxlength="100" formControlName="passwordConfirm" />
</div>
</div>
</div>
</sqx-panel>
</form>

6
src/Squidex/app/features/administration/pages/users/user-page.component.scss

@ -0,0 +1,6 @@
@import '_vars';
@import '_mixins';
.form-group-password {
margin-top: 2rem;
}

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

@ -0,0 +1,157 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { Component, OnInit } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import {
ComponentBase,
MessageBus,
NotificationService,
UserDto,
UserManagementService
} from 'shared';
import { UserCreated, UserUpdated } from './messages';
@Component({
selector: 'sqx-user-page',
styleUrls: ['./user-page.component.scss'],
templateUrl: './user-page.component.html'
})
export class UserPageComponent extends ComponentBase implements OnInit {
public userFormSubmitted = false;
public userForm: FormGroup;
public userId: string;
public userFormError: string;
public isNewMode = false;
constructor(notifications: NotificationService,
private readonly formBuilder: FormBuilder,
private readonly messageBus: MessageBus,
private readonly route: ActivatedRoute,
private readonly router: Router,
private readonly userManagementService: UserManagementService
) {
super(notifications);
}
public ngOnInit() {
this.route.data.map(p => p['user'])
.subscribe((user: UserDto) => {
this.populateForm(user);
});
}
public save(publish: boolean) {
this.userFormSubmitted = true;
if (this.userForm.valid) {
this.userForm.disable();
const requestDto = this.userForm.value;
const enable = (message: string) => {
this.userForm.enable();
this.userForm.controls['password'].reset();
this.userForm.controls['passwordConfirm'].reset();
this.userFormSubmitted = false;
this.userFormError = message;
};
const back = () => {
this.router.navigate(['../'], { relativeTo: this.route, replaceUrl: true });
};
if (this.isNewMode) {
this.userManagementService.postUser(requestDto)
.subscribe(created => {
this.messageBus.publish(
new UserCreated(
created.id,
requestDto.email,
requestDto.displayName,
created.pictureUrl));
this.notifyInfo('User created successfully.');
back();
}, error => {
this.notifyError(error);
enable(error.displayMessage);
});
} else {
this.userManagementService.putUser(this.userId, requestDto)
.subscribe(() => {
this.messageBus.publish(
new UserUpdated(
this.userId,
requestDto.email,
requestDto.displayName));
this.notifyInfo('User saved successfully.');
enable(null);
}, error => {
this.notifyError(error);
enable(error.displayMessage);
});
}
} else {
this.notifyError('Content element not valid, please check the field with the red bar on the left in all languages (if localizable).');
}
}
private populateForm(user: UserDto) {
this.userFormError = '';
this.userFormSubmitted = false;
if (user) {
this.isNewMode = false;
this.userId = user.id;
this.userForm =
this.formBuilder.group({
email: [user.email,
[
Validators.email,
Validators.required,
Validators.maxLength(100)
]],
displayName: [user.displayName,
[
Validators.required,
Validators.maxLength(100)
]],
password: ['', []],
passwordConfirm: ['', []]
});
} else {
this.isNewMode = true;
this.userForm =
this.formBuilder.group({
displayName: ['',
[
Validators.required,
Validators.maxLength(100)
]],
email: ['',
[
Validators.email,
Validators.required,
Validators.maxLength(100)
]],
password: ['', [
Validators.required
]],
passwordConfirm: ['', [
Validators.required
]]
});
}
}
}

10
src/Squidex/app/features/administration/pages/users/users-page.component.html

@ -13,6 +13,10 @@
<form class="form-inline" (ngSubmit)="search()"> <form class="form-inline" (ngSubmit)="search()">
<input class="form-control" [formControl]="usersFilter" placeholder="Search for user" /> <input class="form-control" [formControl]="usersFilter" placeholder="Search for user" />
</form> </form>
<button class="btn btn-success" [routerLink]="['new']">
<i class="icon-plus"></i> New
</button>
</div> </div>
<h3 class="panel-title">Users</h3> <h3 class="panel-title">Users</h3>
@ -52,7 +56,7 @@
<tbody> <tbody>
<ng-template ngFor let-user [ngForOf]="usersItems"> <ng-template ngFor let-user [ngForOf]="usersItems">
<tr> <tr [routerLink]="[user.id]" routerLinkActive="active">
<td> <td>
<img class="user-picture" [attr.title]="user.name" [attr.src]="user.pictureUrl" /> <img class="user-picture" [attr.title]="user.name" [attr.src]="user.pictureUrl" />
</td> </td>
@ -92,4 +96,6 @@
</div> </div>
</div> </div>
</div> </div>
</sqx-panel> </sqx-panel>
<router-outlet></router-outlet>

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

@ -5,25 +5,32 @@
* Copyright (c) Sebastian Stehle. All rights reserved * Copyright (c) Sebastian Stehle. All rights reserved
*/ */
import { Component, OnInit } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms'; import { FormControl } from '@angular/forms';
import { Subscription } from 'rxjs';
import { import {
AuthService, AuthService,
ComponentBase, ComponentBase,
ImmutableArray, ImmutableArray,
MessageBus,
NotificationService, NotificationService,
Pager, Pager,
UserDto, UserDto,
UserManagementService UserManagementService
} from 'shared'; } from 'shared';
import { UserCreated, UserUpdated } from './messages';
@Component({ @Component({
selector: 'sqx-users-page', selector: 'sqx-users-page',
styleUrls: ['./users-page.component.scss'], styleUrls: ['./users-page.component.scss'],
templateUrl: './users-page.component.html' templateUrl: './users-page.component.html'
}) })
export class UsersPageComponent extends ComponentBase implements OnInit { export class UsersPageComponent extends ComponentBase implements OnDestroy, OnInit {
private userCreatedSubscription: Subscription;
private userUpdatedSubscription: Subscription;
public currentUserId: string; public currentUserId: string;
public usersItems = ImmutableArray.empty<UserDto>(); public usersItems = ImmutableArray.empty<UserDto>();
@ -33,12 +40,36 @@ export class UsersPageComponent extends ComponentBase implements OnInit {
constructor(notifications: NotificationService, constructor(notifications: NotificationService,
private readonly userManagementService: UserManagementService, private readonly userManagementService: UserManagementService,
private readonly authService: AuthService private readonly authService: AuthService,
private readonly messageBus: MessageBus
) { ) {
super(notifications); super(notifications);
} }
public ngOnDestroy() {
this.userCreatedSubscription.unsubscribe();
this.userUpdatedSubscription.unsubscribe();
}
public ngOnInit() { public ngOnInit() {
this.userCreatedSubscription =
this.messageBus.of(UserCreated)
.subscribe(message => {
const user = new UserDto(message.id, message.email, message.displayName, message.pictureUrl, false);
this.usersItems = this.usersItems.pushFront(user);
this.usersPager = this.usersPager.incrementCount();
});
this.userUpdatedSubscription =
this.messageBus.of(UserUpdated)
.subscribe(message => {
this.usersItems =
this.usersItems.replaceAll(
u => u.id === message.id,
u => new UserDto(u.id, message.email, message.displayName, u.pictureUrl, u.isLocked));
});
this.currentUserId = this.authService.user!.id; this.currentUserId = this.authService.user!.id;
this.load(); this.load();

20
src/Squidex/app/features/content/pages/content/content-page.component.ts

@ -139,27 +139,29 @@ export class ContentPageComponent extends AppComponentBase implements CanCompone
if (this.contentForm.valid) { if (this.contentForm.valid) {
this.disable(); this.disable();
const data = this.contentForm.value; const requestDto = this.contentForm.value;
const back = () => {
this.router.navigate(['../'], { relativeTo: this.route, replaceUrl: true });
};
if (this.isNewMode) { if (this.isNewMode) {
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.contentsService.postContent(app, this.schema.name, data, publish, this.version)) .switchMap(app => this.contentsService.postContent(app, this.schema.name, requestDto, publish, this.version))
.subscribe(created => { .subscribe(created => {
this.contentId = created.id;
this.messageBus.publish(new ContentCreated(created.id, created.data, this.version.value, publish)); this.messageBus.publish(new ContentCreated(created.id, created.data, this.version.value, publish));
this.notifyInfo('Content created successfully.'); this.notifyInfo('Content created successfully.');
this.finish(); back();
}, error => { }, error => {
this.notifyError(error); this.notifyError(error);
this.enable(); this.enable();
}); });
} else { } else {
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.contentsService.putContent(app, this.schema.name, this.contentId!, data, this.version)) .switchMap(app => this.contentsService.putContent(app, this.schema.name, this.contentId!, requestDto, this.version))
.subscribe(() => { .subscribe(() => {
this.messageBus.publish(new ContentUpdated(this.contentId!, data, this.version.value)); this.messageBus.publish(new ContentUpdated(this.contentId!, requestDto, this.version.value));
this.notifyInfo('Content saved successfully.'); this.notifyInfo('Content saved successfully.');
this.enable(); this.enable();
@ -173,10 +175,6 @@ export class ContentPageComponent extends AppComponentBase implements CanCompone
} }
} }
private finish() {
this.router.navigate(['../'], { relativeTo: this.route, replaceUrl: true });
}
private enable() { private enable() {
this.contentForm.markAsPristine(); this.contentForm.markAsPristine();

2
src/Squidex/app/features/content/pages/contents/contents-page.component.html

@ -18,7 +18,7 @@
<sqx-language-selector class="languages-buttons" (selectedLanguageChanged)="selectLanguage($event)" [languages]="languages"></sqx-language-selector> <sqx-language-selector class="languages-buttons" (selectedLanguageChanged)="selectLanguage($event)" [languages]="languages"></sqx-language-selector>
</span> </span>
<button class="btn btn-success" (click)="gotoNew()"> <button class="btn btn-success" [routerLink]="['new']">
<i class="icon-plus"></i> New <i class="icon-plus"></i> New
</button> </button>
</div> </div>

7
src/Squidex/app/features/content/pages/contents/contents-page.component.ts

@ -7,7 +7,7 @@
import { Component, OnDestroy, OnInit } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms'; import { FormControl } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { import {
@ -59,7 +59,6 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy
private readonly authService: AuthService, private readonly authService: AuthService,
private readonly contentsService: ContentsService, private readonly contentsService: ContentsService,
private readonly route: ActivatedRoute, private readonly route: ActivatedRoute,
private readonly router: Router,
private readonly messageBus: MessageBus private readonly messageBus: MessageBus
) { ) {
super(notifications, apps); super(notifications, apps);
@ -188,10 +187,6 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy
this.load(); this.load();
} }
public gotoNew() {
this.router.navigate(['./new'], { relativeTo: this.route });
}
private updateContents(id: string, p: boolean | undefined, data: any, version: string) { private updateContents(id: string, p: boolean | undefined, data: any, version: string) {
this.contentItems = this.contentItems.replaceAll(x => x.id === id, c => this.updateContent(c, p === undefined ? c.isPublished : p, data, version)); this.contentItems = this.contentItems.replaceAll(x => x.id === id, c => this.updateContent(c, p === undefined ? c.isPublished : p, data, version));
} }

8
src/Squidex/app/features/schemas/pages/schema/schema-edit-form.component.ts

@ -75,6 +75,11 @@ export class SchemaEditFormComponent implements OnInit {
if (this.editForm.valid) { if (this.editForm.valid) {
this.editForm.disable(); this.editForm.disable();
const enable = () => {
this.editForm.enable();
this.editFormSubmitted = false;
};
const requestDto = this.editForm.value; const requestDto = this.editForm.value;
this.schemas.putSchema(this.appName, this.name, requestDto, this.version) this.schemas.putSchema(this.appName, this.name, requestDto, this.version)
@ -82,7 +87,7 @@ export class SchemaEditFormComponent implements OnInit {
this.reset(); this.reset();
this.saved.emit(new SchemaPropertiesDto(requestDto.label, requestDto.hints)); this.saved.emit(new SchemaPropertiesDto(requestDto.label, requestDto.hints));
}, error => { }, error => {
this.editForm.enable(); enable();
this.notifications.notify(Notification.error(error.displayMessage)); this.notifications.notify(Notification.error(error.displayMessage));
}); });
} }
@ -91,5 +96,6 @@ export class SchemaEditFormComponent implements OnInit {
private reset() { private reset() {
this.editFormSubmitted = false; this.editFormSubmitted = false;
this.editForm.reset(); this.editForm.reset();
this.editForm.enable();
} }
} }

4
src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts

@ -195,10 +195,10 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit {
} }
public saveField(field: FieldDto, newField: FieldDto) { public saveField(field: FieldDto, newField: FieldDto) {
const request = new UpdateFieldDto(newField.properties); const requestDto = new UpdateFieldDto(newField.properties);
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.schemasService.putField(app, this.schemaName, field.fieldId, request, this.schemaVersion)).retry(2) .switchMap(app => this.schemasService.putField(app, this.schemaName, field.fieldId, requestDto, this.schemaVersion)).retry(2)
.subscribe(() => { .subscribe(() => {
this.updateField(field, new FieldDto(field.fieldId, field.name, newField.isHidden, field.isDisabled, field.partitioning, newField.properties)); this.updateField(field, new FieldDto(field.fieldId, field.name, newField.isHidden, field.isDisabled, field.partitioning, newField.properties));
}, error => { }, error => {

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

@ -125,12 +125,12 @@ export class ContributorsPageComponent extends AppComponentBase implements OnIni
} }
public assignContributor() { public assignContributor() {
const newContributor = new AppContributorDto(this.addContributorForm.get('user')!.value.model.id, 'Editor'); const requestDto = new AppContributorDto(this.addContributorForm.get('user')!.value.model.id, 'Editor');
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.appContributorsService.postContributor(app, newContributor, this.version)) .switchMap(app => this.appContributorsService.postContributor(app, requestDto, this.version))
.subscribe(() => { .subscribe(() => {
this.updateContributors(this.appContributors.push(newContributor)); this.updateContributors(this.appContributors.push(requestDto));
}, error => { }, error => {
this.notifyError(error); this.notifyError(error);
}); });
@ -139,12 +139,12 @@ export class ContributorsPageComponent extends AppComponentBase implements OnIni
} }
public changePermission(contributor: AppContributorDto, permission: string) { public changePermission(contributor: AppContributorDto, permission: string) {
const newContributor = changePermission(contributor, permission); const requestDto = changePermission(contributor, permission);
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.appContributorsService.postContributor(app, newContributor, this.version)) .switchMap(app => this.appContributorsService.postContributor(app, requestDto, this.version))
.subscribe(() => { .subscribe(() => {
this.updateContributors(this.appContributors.replace(contributor, newContributor)); this.updateContributors(this.appContributors.replace(contributor, requestDto));
}, error => { }, error => {
this.notifyError(error); this.notifyError(error);
}); });

4
src/Squidex/app/features/settings/pages/languages/languages-page.component.ts

@ -77,10 +77,10 @@ export class LanguagesPageComponent extends AppComponentBase implements OnInit {
} }
public addLanguage() { public addLanguage() {
const request = new AddAppLanguageDto(this.addLanguageForm.get('language')!.value.iso2Code); const requestDto = new AddAppLanguageDto(this.addLanguageForm.get('language')!.value.iso2Code);
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.appLanguagesService.postLanguages(app, request, this.version)) .switchMap(app => this.appLanguagesService.postLanguages(app, requestDto, this.version))
.subscribe(dto => { .subscribe(dto => {
this.updateLanguages(this.appLanguages.push(dto)); this.updateLanguages(this.appLanguages.push(dto));
}, error => { }, error => {

7
src/Squidex/app/shared/components/app-form.component.ts

@ -66,12 +66,17 @@ export class AppFormComponent {
const request = new CreateAppDto(this.createForm.get('name')!.value); const request = new CreateAppDto(this.createForm.get('name')!.value);
const enable = () => {
this.createForm.enable();
this.createFormSubmitted = false;
};
this.appsStore.createApp(request) this.appsStore.createApp(request)
.subscribe(dto => { .subscribe(dto => {
this.reset(); this.reset();
this.created.emit(dto); this.created.emit(dto);
}, error => { }, error => {
this.createForm.enable(); enable();
this.creationError = error.displayMessage; this.creationError = error.displayMessage;
}); });
} }

7
src/Squidex/app/shared/components/asset.component.ts

@ -164,18 +164,17 @@ export class AssetComponent extends AppComponentBase implements OnInit {
if (this.renameForm.valid) { if (this.renameForm.valid) {
this.renameForm.disable(); this.renameForm.disable();
const dto = new UpdateAssetDto(this.renameForm.controls['name'].value); const requestDto = new UpdateAssetDto(this.renameForm.controls['name'].value);
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.assetsService.putAsset(app, this.asset.id, dto, this.version)) .switchMap(app => this.assetsService.putAsset(app, this.asset.id, requestDto, this.version))
.subscribe(_ => { .subscribe(_ => {
const me = `subject:${this.authService.user!.id}`; const me = `subject:${this.authService.user!.id}`;
const asset = new AssetDto( const asset = new AssetDto(
this.asset.id, this.asset.id,
this.asset.createdBy, me, this.asset.createdBy, me,
this.asset.created, DateTime.now(), this.asset.created, DateTime.now(), requestDto.fileName,
dto.fileName,
this.asset.fileSize, this.asset.fileSize,
this.asset.fileVersion, this.asset.fileVersion,
this.asset.mimeType, this.asset.mimeType,

1
src/Squidex/app/shared/declarations-base.ts

@ -12,6 +12,7 @@ export * from './guards/resolve-app-languages.guard';
export * from './guards/resolve-content.guard'; export * from './guards/resolve-content.guard';
export * from './guards/resolve-published-schema.guard'; export * from './guards/resolve-published-schema.guard';
export * from './guards/resolve-schema.guard'; export * from './guards/resolve-schema.guard';
export * from './guards/resolve-user.guard';
export * from './services/app-contributors.service'; export * from './services/app-contributors.service';
export * from './services/app-clients.service'; export * from './services/app-clients.service';

85
src/Squidex/app/shared/guards/resolve-user.guard.spec.ts

@ -0,0 +1,85 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { IMock, Mock } from 'typemoq';
import { Observable } from 'rxjs';
import { UserManagementService } from 'shared';
import { ResolveUserGuard } from './resolve-user.guard';
import { RouterMockup } from './router-mockup';
describe('ResolveUserGuard', () => {
const route = {
parent: {
params: {
userId: 'my-user'
}
}
};
let usersService: IMock<UserManagementService>;
beforeEach(() => {
usersService = Mock.ofType(UserManagementService);
});
it('should throw if route does not contain parameter', () => {
const guard = new ResolveUserGuard(usersService.object, <any>new RouterMockup());
expect(() => guard.resolve(<any>{ params: {} }, <any>{})).toThrow('Route must contain app and user name.');
});
it('should navigate to 404 page if user is not found', (done) => {
usersService.setup(x => x.getUser('my-user'))
.returns(() => Observable.of(null!));
const router = new RouterMockup();
const guard = new ResolveUserGuard(usersService.object, <any>router);
guard.resolve(<any>route, <any>{})
.then(result => {
expect(result).toBeFalsy();
expect(router.lastNavigation).toEqual(['/404']);
done();
});
});
it('should navigate to 404 page if user loading fails', (done) => {
usersService.setup(x => x.getUser('my-user'))
.returns(() => Observable.throw(null!));
const router = new RouterMockup();
const guard = new ResolveUserGuard(usersService.object, <any>router);
guard.resolve(<any>route, <any>{})
.then(result => {
expect(result).toBeFalsy();
expect(router.lastNavigation).toEqual(['/404']);
done();
});
});
it('should return user if loading succeeded', (done) => {
const user = {};
usersService.setup(x => x.getUser('my-user'))
.returns(() => Observable.of(user));
const router = new RouterMockup();
const guard = new ResolveUserGuard(usersService.object, <any>router);
guard.resolve(<any>route, <any>{})
.then(result => {
expect(result).toBe(user);
done();
});
});
});

62
src/Squidex/app/shared/guards/resolve-user.guard.ts

@ -0,0 +1,62 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router';
import { UserDto, UserManagementService } from './../services/users.service';
@Injectable()
export class ResolveUserGuard implements Resolve<UserDto> {
constructor(
private readonly userManagementService: UserManagementService,
private readonly router: Router
) {
}
public resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<UserDto> {
const userId = this.findParameter(route, 'userId');
if (!userId) {
throw 'Route must contain user id.';
}
const result =
this.userManagementService.getUser(userId).toPromise()
.then(dto => {
if (!dto) {
this.router.navigate(['/404']);
return null;
}
return dto;
}).catch(() => {
this.router.navigate(['/404']);
return null;
});
return result;
}
private findParameter(route: ActivatedRouteSnapshot, name: string): string | null {
let result: string | null = null;
while (route) {
result = route.params[name];
if (result || !route.parent) {
break;
}
route = route.parent;
}
return result;
}
}

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

@ -7,7 +7,6 @@
import { ModuleWithProviders, NgModule } from '@angular/core'; import { ModuleWithProviders, NgModule } from '@angular/core';
import { DndModule } from 'ng2-dnd'; import { DndModule } from 'ng2-dnd';
import { ProgressHttpModule } from 'angular-progress-http'; import { ProgressHttpModule } from 'angular-progress-http';
import { SqxFrameworkModule } from 'framework'; import { SqxFrameworkModule } from 'framework';
@ -39,6 +38,7 @@ import {
ResolvePublishedSchemaGuard, ResolvePublishedSchemaGuard,
ResolveSchemaGuard, ResolveSchemaGuard,
SchemasService, SchemasService,
ResolveUserGuard,
UsagesService, UsagesService,
UserEmailPipe, UserEmailPipe,
UserEmailRefPipe, UserEmailRefPipe,
@ -110,6 +110,7 @@ export class SqxSharedModule {
ResolveContentGuard, ResolveContentGuard,
ResolvePublishedSchemaGuard, ResolvePublishedSchemaGuard,
ResolveSchemaGuard, ResolveSchemaGuard,
ResolveUserGuard,
SchemasService, SchemasService,
UsagesService, UsagesService,
UserManagementService, UserManagementService,

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

@ -235,6 +235,34 @@ describe('UserManagementService', () => {
authService.verifyAll(); authService.verifyAll();
}); });
it('should make get request to get single user', () => {
authService.setup(x => x.authGet('http://service/p/api/users/123'))
.returns(() => Observable.of(
new Response(
new ResponseOptions({
body: {
id: '123',
email: 'mail1@domain.com',
displayName: 'User1',
pictureUrl: 'path/to/image1',
isLocked: true
}
})
)
))
.verifiable(Times.once());
let user: UserDto | null = null;
userManagementService.getUser('123').subscribe(result => {
user = result;
}).unsubscribe();
expect(user).toEqual(new UserDto('123', 'mail1@domain.com', 'User1', 'path/to/image1', true));
authService.verifyAll();
});
it('should make put request to lock user', () => { it('should make put request to lock user', () => {
authService.setup(x => x.authPut('http://service/p/api/user-management/123/lock', It.isAny())) authService.setup(x => x.authPut('http://service/p/api/user-management/123/lock', It.isAny()))
.returns(() => Observable.of( .returns(() => Observable.of(

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

@ -21,6 +21,32 @@ export class UsersDto {
} }
} }
export class UserCreatedDto {
constructor(
public readonly id: string,
public readonly pictureUrl: string
) {
}
}
export class CreateUserDto {
constructor(
public readonly email: string,
public readonly displayName: string,
public readonly password: string
) {
}
}
export class UpdateUserDto {
constructor(
public readonly email: string,
public readonly displayName: string,
public readonly password: string
) {
}
}
export class UserDto { export class UserDto {
constructor( constructor(
public readonly id: string, public readonly id: string,
@ -107,6 +133,38 @@ export class UserManagementService {
.catchError('Failed to load users. Please reload.'); .catchError('Failed to load users. Please reload.');
} }
public getUser(id: string): Observable<UserDto> {
const url = this.apiUrl.buildUrl(`api/user-management/${id}`);
return this.authService.authGet(url)
.map(response => response.json())
.map(response => {
return new UserDto(
response.id,
response.email,
response.displayName,
response.pictureUrl,
response.isLocked);
})
.catchError('Failed to load user. Please reload.');
}
public postUser(dto: CreateUserDto): Observable<UserDto> {
const url = this.apiUrl.buildUrl(`api/user-management/`);
return this.authService.authPost(url, dto)
.map(response => response.json())
.map(response => new UserCreatedDto(response.id, response.pictureUrl))
.catchError('Failed to create user. Please reload.');
}
public putUser(id: string, dto: UpdateUserDto): Observable<any> {
const url = this.apiUrl.buildUrl(`api/user-management/${id}`);
return this.authService.authPut(url, dto)
.catchError('Failed to update user. Please reload.');
}
public lockUser(id: string): Observable<any> { public lockUser(id: string): Observable<any> {
const url = this.apiUrl.buildUrl(`api/user-management/${id}/lock`); const url = this.apiUrl.buildUrl(`api/user-management/${id}/lock`);

15
src/Squidex/app/theme/_lists.scss

@ -60,9 +60,18 @@
} }
&.active { &.active {
background: $color-theme-blue; & {
border-color: $color-theme-blue; background: $color-theme-blue;
color: $color-dark-foreground; border-color: $color-theme-blue;
color: $color-dark-foreground;
}
.btn-link {
&,
&:hover {
color: $color-dark-foreground;
}
}
} }
} }
} }

8
tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs

@ -87,7 +87,7 @@ namespace Squidex.Write.Apps
var context = CreateContextForCommand(new AssignContributor { ContributorId = contributorId }); var context = CreateContextForCommand(new AssignContributor { ContributorId = contributorId });
userResolver.Setup(x => x.FindById(contributorId)).Returns(Task.FromResult<IUser>(null)); userResolver.Setup(x => x.FindByIdAsync(contributorId)).Returns(Task.FromResult<IUser>(null));
await TestUpdate(app, async _ => await TestUpdate(app, async _ =>
{ {
@ -106,7 +106,7 @@ namespace Squidex.Write.Apps
var context = CreateContextForCommand(new AssignContributor { ContributorId = contributorId }); var context = CreateContextForCommand(new AssignContributor { ContributorId = contributorId });
userResolver.Setup(x => x.FindById(It.IsAny<string>())).Returns(Task.FromResult(new Mock<IUser>().Object)); userResolver.Setup(x => x.FindByIdAsync(It.IsAny<string>())).Returns(Task.FromResult(new Mock<IUser>().Object));
await TestUpdate(app, async _ => await TestUpdate(app, async _ =>
{ {
@ -121,7 +121,7 @@ namespace Squidex.Write.Apps
var context = CreateContextForCommand(new AssignContributor { ContributorId = contributorId }); var context = CreateContextForCommand(new AssignContributor { ContributorId = contributorId });
userResolver.Setup(x => x.FindById(contributorId)).Returns(Task.FromResult<IUser>(null)); userResolver.Setup(x => x.FindByIdAsync(contributorId)).Returns(Task.FromResult<IUser>(null));
await TestUpdate(app, async _ => await TestUpdate(app, async _ =>
{ {
@ -138,7 +138,7 @@ namespace Squidex.Write.Apps
var context = CreateContextForCommand(new AssignContributor { ContributorId = contributorId }); var context = CreateContextForCommand(new AssignContributor { ContributorId = contributorId });
userResolver.Setup(x => x.FindById(contributorId)).Returns(Task.FromResult(new Mock<IUser>().Object)); userResolver.Setup(x => x.FindByIdAsync(contributorId)).Returns(Task.FromResult(new Mock<IUser>().Object));
await TestUpdate(app, async _ => await TestUpdate(app, async _ =>
{ {

Loading…
Cancel
Save